-
Notifications
You must be signed in to change notification settings - Fork 445
fix(auth): don't block existing OAuth users when ALLOW_REGISTRATION=false #431
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
|
|
||
| const mockUserCount = vi.hoisted(() => vi.fn()); | ||
| const mockInviteFindUnique = vi.hoisted(() => vi.fn()); | ||
|
|
||
| vi.mock('../prisma-client', () => ({ | ||
| db: { | ||
| user: { count: mockUserCount }, | ||
| invite: { findUnique: mockInviteFindUnique }, | ||
| }, | ||
| })); | ||
|
|
||
| import { getIsRegistrationAllowed } from './registration.service'; | ||
|
|
||
| const ORIGINAL_ENV = { ...process.env }; | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| // Not the first user unless a test says otherwise | ||
| mockUserCount.mockResolvedValue(5); | ||
| mockInviteFindUnique.mockResolvedValue(null); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| process.env = { ...ORIGINAL_ENV }; | ||
| }); | ||
|
|
||
| describe('getIsRegistrationAllowed', () => { | ||
| it('allows everything in cloud (ALLOW_REGISTRATION unset)', async () => { | ||
| process.env.ALLOW_REGISTRATION = undefined; | ||
| delete process.env.ALLOW_REGISTRATION; | ||
|
|
||
| await expect(getIsRegistrationAllowed()).resolves.toBe(true); | ||
| expect(mockUserCount).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('allows the very first user even when registration is disabled', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'false'; | ||
| mockUserCount.mockResolvedValue(0); | ||
|
|
||
| await expect(getIsRegistrationAllowed()).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it('blocks a new user with no invite when registration is disabled', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'false'; | ||
|
|
||
| await expect(getIsRegistrationAllowed()).resolves.toBe(false); | ||
| }); | ||
|
|
||
| it('allows a new user holding a valid invite when registration is disabled', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'false'; | ||
| process.env.ALLOW_INVITATION = 'true'; | ||
| mockInviteFindUnique.mockResolvedValue({ id: 'invite-1' }); | ||
|
|
||
| await expect(getIsRegistrationAllowed('invite-1')).resolves.toBe(true); | ||
| }); | ||
|
|
||
| it('blocks an unknown invite id', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'false'; | ||
| process.env.ALLOW_INVITATION = 'true'; | ||
| mockInviteFindUnique.mockResolvedValue(null); | ||
|
|
||
| await expect(getIsRegistrationAllowed('nope')).resolves.toBe(false); | ||
| }); | ||
|
|
||
| it('blocks a valid invite when invitations are disabled', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'false'; | ||
| process.env.ALLOW_INVITATION = 'false'; | ||
| mockInviteFindUnique.mockResolvedValue({ id: 'invite-1' }); | ||
|
|
||
| await expect(getIsRegistrationAllowed('invite-1')).resolves.toBe(false); | ||
| expect(mockInviteFindUnique).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('allows open self-hosted registration', async () => { | ||
| process.env.ALLOW_REGISTRATION = 'true'; | ||
|
|
||
| await expect(getIsRegistrationAllowed()).resolves.toBe(true); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { db } from '../prisma-client'; | ||
|
|
||
| /** | ||
| * Whether a *new* user may be created right now. | ||
| * | ||
| * This must only be consulted at the point where we know the account does not | ||
| * exist yet. Calling it before an identity is known (e.g. when kicking off an | ||
| * OAuth redirect) would reject returning users too, since we cannot tell a | ||
| * sign-in from a sign-up at that stage. | ||
| */ | ||
| export async function getIsRegistrationAllowed(inviteId?: string | null) { | ||
| // ALLOW_REGISTRATION is always undefined in cloud | ||
| if (process.env.ALLOW_REGISTRATION === undefined) { | ||
| return true; | ||
| } | ||
|
|
||
| // Self-hosting logic | ||
| // 1. First user is always allowed | ||
| const count = await db.user.count(); | ||
| if (count === 0) { | ||
| return true; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // 2. If there is an invite, check if it is valid | ||
| if (inviteId) { | ||
| if (process.env.ALLOW_INVITATION === 'false') { | ||
| return false; | ||
| } | ||
|
|
||
| const invite = await db.invite.findUnique({ | ||
| where: { | ||
| id: inviteId, | ||
| }, | ||
| }); | ||
|
|
||
| return !!invite; | ||
| } | ||
|
|
||
| // 3. Otherwise, check if general registration is allowed | ||
| return process.env.ALLOW_REGISTRATION !== 'false'; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not log the OAuth user or raw invite identifier for denied registration.
The callback catch blocks log this
LogError, andLogErrorpreserves its payload. This new payload adds the OAuth identifier, email, names, andinviteIdto application logs when registration is denied.Log only the provider and an opaque correlation value. Hash an identifier if support staff need to correlate failures.
🤖 Prompt for AI Agents