Skip to content

Commit bb9fa4f

Browse files
Add explicit error message when OAuth scopes are incorrect
1 parent f1dd16b commit bb9fa4f

File tree

3 files changed

+64
-6
lines changed

3 files changed

+64
-6
lines changed

packages/backend/src/ee/accountPermissionSyncer.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import * as Sentry from "@sentry/node";
2-
import { PrismaClient, AccountPermissionSyncJobStatus, Account} from "@sourcebot/db";
2+
import { PrismaClient, AccountPermissionSyncJobStatus, Account } from "@sourcebot/db";
33
import { env, hasEntitlement, createLogger } from "@sourcebot/shared";
44
import { Job, Queue, Worker } from "bullmq";
55
import { Redis } from "ioredis";
66
import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js";
7-
import { createOctokitFromToken, getReposForAuthenticatedUser } from "../github.js";
8-
import { createGitLabFromOAuthToken, getProjectsForAuthenticatedUser } from "../gitlab.js";
7+
import {
8+
createOctokitFromToken,
9+
getOAuthScopesForAuthenticatedUser as getGitHubOAuthScopesForAuthenticatedUser,
10+
getReposForAuthenticatedUser,
11+
} from "../github.js";
12+
import {
13+
createGitLabFromOAuthToken,
14+
getOAuthScopesForAuthenticatedUser as getGitLabOAuthScopesForAuthenticatedUser,
15+
getProjectsForAuthenticatedUser,
16+
} from "../gitlab.js";
917
import { Settings } from "../types.js";
1018
import { setIntervalAsync } from "../utils.js";
1119

@@ -163,6 +171,12 @@ export class AccountPermissionSyncer {
163171
token: account.access_token,
164172
url: env.AUTH_EE_GITHUB_BASE_URL,
165173
});
174+
175+
const scopes = await getGitHubOAuthScopesForAuthenticatedUser(octokit);
176+
if (!scopes.includes('repo')) {
177+
throw new Error(`OAuth token with scopes [${scopes.join(', ')}] is missing the 'repo' scope required for permission syncing.`);
178+
}
179+
166180
// @note: we only care about the private repos since we don't need to build a mapping
167181
// for public repos.
168182
// @see: packages/web/src/prisma.ts
@@ -189,6 +203,11 @@ export class AccountPermissionSyncer {
189203
url: env.AUTH_EE_GITLAB_BASE_URL,
190204
});
191205

206+
const scopes = await getGitLabOAuthScopesForAuthenticatedUser(api);
207+
if (!scopes.includes('read_api')) {
208+
throw new Error(`OAuth token with scopes [${scopes.join(', ')}] is missing the 'read_api' scope required for permission syncing.`);
209+
}
210+
192211
// @note: we only care about the private and internal repos since we don't need to build a mapping
193212
// for public repos.
194213
// @see: packages/web/src/prisma.ts

packages/backend/src/github.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,20 @@ export const getReposForAuthenticatedUser = async (visibility: 'all' | 'private'
197197
}
198198
}
199199

200+
// Gets oauth scopes
201+
// @see: https://github.com/octokit/auth-token.js/?tab=readme-ov-file#find-out-what-scopes-are-enabled-for-oauth-tokens
202+
export const getOAuthScopesForAuthenticatedUser = async (octokit: Octokit) => {
203+
try {
204+
const response = await octokit.request("HEAD /");
205+
const scopes = response.headers["x-oauth-scopes"]?.split(/,\s+/) || [];
206+
return scopes;
207+
} catch (error) {
208+
Sentry.captureException(error);
209+
logger.error(`Failed to fetch OAuth scopes for authenticated user.`, error);
210+
throw error;
211+
}
212+
}
213+
200214
const getReposOwnedByUsers = async (users: string[], octokit: Octokit, signal: AbortSignal, url?: string) => {
201215
const results = await Promise.allSettled(users.map((user) => githubQueryLimit(async () => {
202216
try {

packages/backend/src/gitlab.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig) =
4141
const token = config.token ?
4242
await getTokenFromConfig(config.token) :
4343
hostname === GITLAB_CLOUD_HOSTNAME ?
44-
env.FALLBACK_GITLAB_CLOUD_TOKEN :
45-
undefined;
44+
env.FALLBACK_GITLAB_CLOUD_TOKEN :
45+
undefined;
4646

4747
const api = await createGitLabFromPersonalAccessToken({
4848
token,
@@ -202,7 +202,7 @@ export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig) =
202202

203203
return !isExcluded;
204204
});
205-
205+
206206
logger.debug(`Found ${repos.length} total repositories.`);
207207

208208
return {
@@ -311,4 +311,29 @@ export const getProjectsForAuthenticatedUser = async (visibility: 'private' | 'i
311311
logger.error(`Failed to fetch projects for authenticated user.`, error);
312312
throw error;
313313
}
314+
}
315+
316+
// Fetches OAuth scopes for the authenticated user.
317+
// @see: https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo
318+
// @see: https://docs.gitlab.com/api/oauth2/#retrieve-the-token-information
319+
export const getOAuthScopesForAuthenticatedUser = async (api: InstanceType<typeof Gitlab>) => {
320+
try {
321+
const response = await api.requester.get('/oauth/token/info');
322+
console.log('response', response);
323+
if (
324+
response &&
325+
typeof response.body === 'object' &&
326+
response.body !== null &&
327+
'scope' in response.body &&
328+
Array.isArray(response.body.scope)
329+
) {
330+
return response.body.scope;
331+
}
332+
333+
throw new Error('/oauth/token_info response body is not in the expected format.');
334+
} catch (error) {
335+
Sentry.captureException(error);
336+
logger.error('Failed to fetch OAuth scopes for authenticated user.', error);
337+
throw error;
338+
}
314339
}

0 commit comments

Comments
 (0)