mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Compare commits
3 commits
3b2fb0d20d
...
dfbf66fed2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfbf66fed2 | ||
|
|
d022066529 | ||
|
|
cbc2cfc190 |
5 changed files with 178 additions and 83 deletions
|
|
@ -1,11 +1,19 @@
|
||||||
import * as Sentry from "@sentry/node";
|
import * as Sentry from "@sentry/node";
|
||||||
import { PrismaClient, AccountPermissionSyncJobStatus, Account} from "@sourcebot/db";
|
import { PrismaClient, AccountPermissionSyncJobStatus, Account } from "@sourcebot/db";
|
||||||
import { env, hasEntitlement, createLogger } from "@sourcebot/shared";
|
import { env, hasEntitlement, createLogger } from "@sourcebot/shared";
|
||||||
import { Job, Queue, Worker } from "bullmq";
|
import { Job, Queue, Worker } from "bullmq";
|
||||||
import { Redis } from "ioredis";
|
import { Redis } from "ioredis";
|
||||||
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js";
|
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js";
|
||||||
import { createOctokitFromToken, getReposForAuthenticatedUser } from "../github.js";
|
import {
|
||||||
import { createGitLabFromOAuthToken, getProjectsForAuthenticatedUser } from "../gitlab.js";
|
createOctokitFromToken,
|
||||||
|
getOAuthScopesForAuthenticatedUser as getGitHubOAuthScopesForAuthenticatedUser,
|
||||||
|
getReposForAuthenticatedUser,
|
||||||
|
} from "../github.js";
|
||||||
|
import {
|
||||||
|
createGitLabFromOAuthToken,
|
||||||
|
getOAuthScopesForAuthenticatedUser as getGitLabOAuthScopesForAuthenticatedUser,
|
||||||
|
getProjectsForAuthenticatedUser,
|
||||||
|
} from "../gitlab.js";
|
||||||
import { Settings } from "../types.js";
|
import { Settings } from "../types.js";
|
||||||
import { setIntervalAsync } from "../utils.js";
|
import { setIntervalAsync } from "../utils.js";
|
||||||
|
|
||||||
|
|
@ -163,6 +171,12 @@ export class AccountPermissionSyncer {
|
||||||
token: account.access_token,
|
token: account.access_token,
|
||||||
url: env.AUTH_EE_GITHUB_BASE_URL,
|
url: env.AUTH_EE_GITHUB_BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scopes = await getGitHubOAuthScopesForAuthenticatedUser(octokit);
|
||||||
|
if (!scopes.includes('repo')) {
|
||||||
|
throw new Error(`OAuth token with scopes [${scopes.join(', ')}] is missing the 'repo' scope required for permission syncing.`);
|
||||||
|
}
|
||||||
|
|
||||||
// @note: we only care about the private repos since we don't need to build a mapping
|
// @note: we only care about the private repos since we don't need to build a mapping
|
||||||
// for public repos.
|
// for public repos.
|
||||||
// @see: packages/web/src/prisma.ts
|
// @see: packages/web/src/prisma.ts
|
||||||
|
|
@ -189,6 +203,11 @@ export class AccountPermissionSyncer {
|
||||||
url: env.AUTH_EE_GITLAB_BASE_URL,
|
url: env.AUTH_EE_GITLAB_BASE_URL,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const scopes = await getGitLabOAuthScopesForAuthenticatedUser(api);
|
||||||
|
if (!scopes.includes('read_api')) {
|
||||||
|
throw new Error(`OAuth token with scopes [${scopes.join(', ')}] is missing the 'read_api' scope required for permission syncing.`);
|
||||||
|
}
|
||||||
|
|
||||||
// @note: we only care about the private and internal repos since we don't need to build a mapping
|
// @note: we only care about the private and internal repos since we don't need to build a mapping
|
||||||
// for public repos.
|
// for public repos.
|
||||||
// @see: packages/web/src/prisma.ts
|
// @see: packages/web/src/prisma.ts
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,20 @@ export const getReposForAuthenticatedUser = async (visibility: 'all' | 'private'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gets oauth scopes
|
||||||
|
// @see: https://github.com/octokit/auth-token.js/?tab=readme-ov-file#find-out-what-scopes-are-enabled-for-oauth-tokens
|
||||||
|
export const getOAuthScopesForAuthenticatedUser = async (octokit: Octokit) => {
|
||||||
|
try {
|
||||||
|
const response = await octokit.request("HEAD /");
|
||||||
|
const scopes = response.headers["x-oauth-scopes"]?.split(/,\s+/) || [];
|
||||||
|
return scopes;
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
logger.error(`Failed to fetch OAuth scopes for authenticated user.`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal, url?: string) => {
|
const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal, url?: string) => {
|
||||||
const results = await Promise.allSettled(users.map((user) => githubQueryLimit(async () => {
|
const results = await Promise.allSettled(users.map((user) => githubQueryLimit(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -312,3 +312,28 @@ export const getProjectsForAuthenticatedUser = async (visibility: 'private' | 'i
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetches OAuth scopes for the authenticated user.
|
||||||
|
// @see: https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo
|
||||||
|
// @see: https://docs.gitlab.com/api/oauth2/#retrieve-the-token-information
|
||||||
|
export const getOAuthScopesForAuthenticatedUser = async (api: InstanceType<typeof Gitlab>) => {
|
||||||
|
try {
|
||||||
|
const response = await api.requester.get('/oauth/token/info');
|
||||||
|
console.log('response', response);
|
||||||
|
if (
|
||||||
|
response &&
|
||||||
|
typeof response.body === 'object' &&
|
||||||
|
response.body !== null &&
|
||||||
|
'scope' in response.body &&
|
||||||
|
Array.isArray(response.body.scope)
|
||||||
|
) {
|
||||||
|
return response.body.scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('/oauth/token_info response body is not in the expected format.');
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
logger.error('Failed to fetch OAuth scopes for authenticated user.', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -29,9 +29,20 @@ export const AuthMethodSelector = ({
|
||||||
// Call the optional analytics callback first
|
// Call the optional analytics callback first
|
||||||
onProviderClick?.(provider);
|
onProviderClick?.(provider);
|
||||||
|
|
||||||
signIn(provider, {
|
// @nocheckin
|
||||||
redirectTo: callbackUrl ?? "/"
|
signIn(
|
||||||
});
|
provider,
|
||||||
|
{
|
||||||
|
redirectTo: callbackUrl ?? "/",
|
||||||
|
},
|
||||||
|
// @see: https://github.com/nextauthjs/next-auth/issues/2066
|
||||||
|
// @see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest
|
||||||
|
// @see: https://next-auth.js.org/getting-started/client#additional-parameters
|
||||||
|
{
|
||||||
|
prompt: 'consent',
|
||||||
|
scope: 'read:user user:email repo'
|
||||||
|
}
|
||||||
|
);
|
||||||
}, [callbackUrl, onProviderClick]);
|
}, [callbackUrl, onProviderClick]);
|
||||||
|
|
||||||
// Separate OAuth providers from special auth methods
|
// Separate OAuth providers from special auth methods
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,8 @@ export const getProviders = () => {
|
||||||
const providers: IdentityProvider[] = eeIdentityProviders;
|
const providers: IdentityProvider[] = eeIdentityProviders;
|
||||||
|
|
||||||
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
|
if (env.SMTP_CONNECTION_URL && env.EMAIL_FROM_ADDRESS && env.AUTH_EMAIL_CODE_LOGIN_ENABLED === 'true') {
|
||||||
providers.push({ provider: EmailProvider({
|
providers.push({
|
||||||
|
provider: EmailProvider({
|
||||||
server: env.SMTP_CONNECTION_URL,
|
server: env.SMTP_CONNECTION_URL,
|
||||||
from: env.EMAIL_FROM_ADDRESS,
|
from: env.EMAIL_FROM_ADDRESS,
|
||||||
maxAge: 60 * 10,
|
maxAge: 60 * 10,
|
||||||
|
|
@ -84,11 +85,13 @@ export const getProviders = () => {
|
||||||
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
|
throw new Error(`Email(s) (${failed.join(", ")}) could not be sent`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), purpose: "sso"});
|
}), purpose: "sso"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') {
|
if (env.AUTH_CREDENTIALS_LOGIN_ENABLED === 'true') {
|
||||||
providers.push({ provider: Credentials({
|
providers.push({
|
||||||
|
provider: Credentials({
|
||||||
credentials: {
|
credentials: {
|
||||||
email: {},
|
email: {},
|
||||||
password: {}
|
password: {}
|
||||||
|
|
@ -141,7 +144,8 @@ export const getProviders = () => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), purpose: "sso"});
|
}), purpose: "sso"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return providers;
|
return providers;
|
||||||
|
|
@ -156,7 +160,29 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||||
trustHost: true,
|
trustHost: true,
|
||||||
events: {
|
events: {
|
||||||
createUser: onCreateUser,
|
createUser: onCreateUser,
|
||||||
signIn: async ({ user }) => {
|
signIn: async ({ user, account }) => {
|
||||||
|
// Explicitly update the Account record with the OAuth token details.
|
||||||
|
// This is necessary to update the access token when the user
|
||||||
|
// re-authenticates.
|
||||||
|
if (account && account.provider && account.providerAccountId) {
|
||||||
|
await prisma.account.update({
|
||||||
|
where: {
|
||||||
|
provider_providerAccountId: {
|
||||||
|
provider: account.provider,
|
||||||
|
providerAccountId: account.providerAccountId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
refresh_token: account.refresh_token,
|
||||||
|
access_token: account.access_token,
|
||||||
|
expires_at: account.expires_at,
|
||||||
|
token_type: account.token_type,
|
||||||
|
scope: account.scope,
|
||||||
|
id_token: account.id_token,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (user.id) {
|
if (user.id) {
|
||||||
await auditService.createAudit({
|
await auditService.createAudit({
|
||||||
action: "user.signed_in",
|
action: "user.signed_in",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue