From 0e498e0e40a4f4035fbf4d7f2aac4f3359f9c479 Mon Sep 17 00:00:00 2001 From: sutterj Date: Wed, 26 Jun 2024 15:58:09 -0400 Subject: [PATCH] feat: check for allowed orgs --- .env.example | 2 + env.mjs | 9 ++++ src/app/api/auth/lib/nextauth-options.ts | 52 ++++++++++++++++++++---- 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index 1fec674..dfb5c6a 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,8 @@ NEXTAUTH_SECRET=bad-secret NEXTAUTH_URL=http://localhost:3000 # A comma-separated list of GitHub usernames that are allowed to access the app ALLOWED_HANDLES= +# A comma-separated list of GitHub orgs that are allowed to access the app +ALLOWED_ORGS= # This is used to sign payloads from github, see this doc for more info # https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries diff --git a/env.mjs b/env.mjs index f488b5d..b862dbf 100644 --- a/env.mjs +++ b/env.mjs @@ -31,6 +31,14 @@ export const env = createEnv({ if (val === '') return true return val.split(',').every((handle) => handle.trim().length > 0) }, 'Invalid comma separated list of GitHub handles'), + ALLOWED_ORGS: z + .string() + .optional() + .default('') + .refine((val) => { + if (val === '') return true + return val.split(',').every((org) => org.trim().length > 0) + }, 'Invalid comma separated list of GitHub orgs'), }, /* * Environment variables available on the client (and server). @@ -57,6 +65,7 @@ export const env = createEnv({ PUBLIC_ORG: process.env.PUBLIC_ORG, PRIVATE_ORG: process.env.PRIVATE_ORG, ALLOWED_HANDLES: process.env.ALLOWED_HANDLES, + ALLOWED_ORGS: process.env.ALLOWED_ORGS, }, skipValidation: process.env.SKIP_ENV_VALIDATIONS === 'true', }) diff --git a/src/app/api/auth/lib/nextauth-options.ts b/src/app/api/auth/lib/nextauth-options.ts index a0040b5..eeab05a 100644 --- a/src/app/api/auth/lib/nextauth-options.ts +++ b/src/app/api/auth/lib/nextauth-options.ts @@ -131,35 +131,71 @@ export const nextAuthOptions: AuthOptions = { }, }, callbacks: { - signIn: (params) => { + signIn: async (params) => { authLogger.debug('Sign in callback') const profile = params.profile as Profile & { login?: string } + + // If there is no login, prevent sign in + if (!profile?.login) { + return false + } + + // Get the allowed handles list const allowedHandles = ( process.env.ALLOWED_HANDLES?.split(',') ?? [] ).filter((handle) => handle !== '') - if (allowedHandles.length === 0) { + // Get the allowed orgs list + const allowedOrgs = (process.env.ALLOWED_ORGS?.split(',') ?? []).filter( + (org) => org !== '', + ) + + // If there are no allowed handles and no allowed orgs specified, allow all users + if (allowedHandles.length === 0 && allowedOrgs.length === 0) { authLogger.info( - 'No allowed handles specified via ALLOWED_HANDLES, allowing all users.', + 'No allowed handles or orgs specified, allowing all users.', ) - return true - } - if (!profile?.login) { - return false + return true } authLogger.debug('Trying to sign in with handle:', profile.login) + // If the user is in the allowed handles list, allow sign in if (allowedHandles.includes(profile.login)) { return true } - authLogger.warn( + authLogger.debug( `User "${profile.login}" is not in the allowed handles list`, ) + authLogger.debug( + "Checking if any of user's orgs are in allowed orgs list", + ) + + const octokit = personalOctokit(params.account?.access_token as string) + + // Get the user's organizations + const orgs = await octokit + .paginate(octokit.rest.orgs.listForAuthenticatedUser) + .catch((error: Error) => { + authLogger.error('Failed to fetch organizations', { error }) + return [] + }) + + // Check if any of the user's organizations are in the allowed orgs list + if (orgs.some((org) => allowedOrgs.includes(org.login))) { + authLogger.info( + `User "${profile.login}" has an org in the allowed orgs list`, + ) + + return true + } + + authLogger.warn(`User "${profile.login}" is not allowed to sign in`) + return false }, session: ({ session, token }) => {