Skip to content

Commit 19631cf

Browse files
refactor(auth): drop bannedEmails; gate AppConfig on isHosted
- Remove the bannedEmails denylist entirely (better-auth banning + blockedSignupDomains cover the cases). - Move isAppConfigEnabled into feature-flags.ts and gate it on isHosted, so AppConfig is hosted-only; self-hosted/OSS always uses the env-var fallback and never constructs the AWS client.
1 parent 9ee5b1f commit 19631cf

5 files changed

Lines changed: 35 additions & 42 deletions

File tree

apps/sim/lib/auth/access-control.test.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,17 @@
44
import { beforeEach, describe, expect, it, vi } from 'vitest'
55
import type { AccessControlConfig } from '@/lib/auth/access-control'
66

7-
const { mockFetch, envRef } = vi.hoisted(() => ({
7+
const { mockFetch, envRef, flagRef } = vi.hoisted(() => ({
88
mockFetch: vi.fn(),
99
envRef: {
10-
APPCONFIG_APPLICATION: undefined as string | undefined,
11-
APPCONFIG_ENVIRONMENT: undefined as string | undefined,
10+
APPCONFIG_APPLICATION: 'sim-staging' as string | undefined,
11+
APPCONFIG_ENVIRONMENT: 'staging' as string | undefined,
1212
BLOCKED_SIGNUP_DOMAINS: undefined as string | undefined,
1313
ALLOWED_LOGIN_EMAILS: undefined as string | undefined,
1414
ALLOWED_LOGIN_DOMAINS: undefined as string | undefined,
1515
BLOCKED_EMAIL_MX_HOSTS: undefined as string | undefined,
1616
},
17+
flagRef: { isAppConfigEnabled: false },
1718
}))
1819

1920
vi.mock('@/lib/core/config/appconfig', () => ({
@@ -26,30 +27,34 @@ vi.mock('@/lib/core/config/env', () => ({
2627
},
2728
}))
2829

30+
vi.mock('@/lib/core/config/feature-flags', () => ({
31+
get isAppConfigEnabled() {
32+
return flagRef.isAppConfigEnabled
33+
},
34+
}))
35+
2936
import { getAccessControlConfig } from '@/lib/auth/access-control'
3037

3138
const empty: AccessControlConfig = {
3239
blockedSignupDomains: [],
3340
allowedLoginEmails: [],
3441
allowedLoginDomains: [],
3542
blockedEmailMxHosts: [],
36-
bannedEmails: [],
3743
}
3844

3945
describe('getAccessControlConfig', () => {
4046
beforeEach(() => {
4147
vi.clearAllMocks()
48+
flagRef.isAppConfigEnabled = false
4249
Object.assign(envRef, {
43-
APPCONFIG_APPLICATION: undefined,
44-
APPCONFIG_ENVIRONMENT: undefined,
4550
BLOCKED_SIGNUP_DOMAINS: undefined,
4651
ALLOWED_LOGIN_EMAILS: undefined,
4752
ALLOWED_LOGIN_DOMAINS: undefined,
4853
BLOCKED_EMAIL_MX_HOSTS: undefined,
4954
})
5055
})
5156

52-
describe('env fallback (AppConfig not configured)', () => {
57+
describe('env fallback (AppConfig disabled)', () => {
5358
it('returns empty lists when nothing is set', async () => {
5459
expect(await getAccessControlConfig()).toEqual(empty)
5560
expect(mockFetch).not.toHaveBeenCalled()
@@ -61,15 +66,13 @@ describe('getAccessControlConfig', () => {
6166
const result = await getAccessControlConfig()
6267
expect(result.blockedSignupDomains).toEqual(['gmail.com', 'yahoo.com'])
6368
expect(result.allowedLoginDomains).toEqual(['sim.ai'])
64-
expect(result.bannedEmails).toEqual([])
6569
expect(mockFetch).not.toHaveBeenCalled()
6670
})
6771
})
6872

69-
describe('AppConfig source', () => {
73+
describe('AppConfig source (enabled)', () => {
7074
beforeEach(() => {
71-
envRef.APPCONFIG_APPLICATION = 'sim-staging'
72-
envRef.APPCONFIG_ENVIRONMENT = 'staging'
75+
flagRef.isAppConfigEnabled = true
7376
})
7477

7578
it('reads the access-control profile and normalizes the payload', async () => {
@@ -78,7 +81,6 @@ describe('getAccessControlConfig', () => {
7881
parse({
7982
blockedSignupDomains: ['X.com'],
8083
allowedLoginDomains: ['sim.ai'],
81-
bannedEmails: ['A@B.com', 'a@b.com', ' '],
8284
blockedEmailMxHosts: 'not-an-array',
8385
})
8486
)
@@ -87,7 +89,6 @@ describe('getAccessControlConfig', () => {
8789
const result = await getAccessControlConfig()
8890
expect(result.blockedSignupDomains).toEqual(['x.com'])
8991
expect(result.allowedLoginDomains).toEqual(['sim.ai'])
90-
expect(result.bannedEmails).toEqual(['a@b.com'])
9192
expect(result.blockedEmailMxHosts).toEqual([])
9293
expect(mockFetch).toHaveBeenCalledWith(
9394
{ application: 'sim-staging', environment: 'staging', profile: 'access-control' },

apps/sim/lib/auth/access-control.ts

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { fetchAppConfigProfile } from '@/lib/core/config/appconfig'
22
import { env } from '@/lib/core/config/env'
3+
import { isAppConfigEnabled } from '@/lib/core/config/feature-flags'
34

45
/**
56
* Name of the AppConfig configuration profile holding the signup/login gating
@@ -10,15 +11,14 @@ const ACCESS_CONTROL_PROFILE = 'access-control'
1011

1112
/**
1213
* Normalized signup/login gating lists. All entries are trimmed, lowercased, and
13-
* de-duplicated. Domains are bare hostnames; emails are full addresses; MX hosts
14-
* are substrings matched against resolved MX exchanges.
14+
* de-duplicated. Domains are bare hostnames; MX hosts are substrings matched
15+
* against resolved MX exchanges; emails are full addresses.
1516
*/
1617
export interface AccessControlConfig {
1718
blockedSignupDomains: string[]
1819
allowedLoginEmails: string[]
1920
allowedLoginDomains: string[]
2021
blockedEmailMxHosts: string[]
21-
bannedEmails: string[]
2222
}
2323

2424
function normalizeList(values: unknown): string[] {
@@ -32,16 +32,14 @@ function parseCsv(value: string | undefined): string[] {
3232

3333
/**
3434
* Fallback source for self-hosted/OSS/local deployments that have no AppConfig.
35-
* Reads the same env vars the app used before AppConfig. There is no env
36-
* equivalent for `bannedEmails` — that list is AppConfig-only.
35+
* Reads the same env vars the app used before AppConfig.
3736
*/
3837
function fromEnv(): AccessControlConfig {
3938
return {
4039
blockedSignupDomains: parseCsv(env.BLOCKED_SIGNUP_DOMAINS),
4140
allowedLoginEmails: parseCsv(env.ALLOWED_LOGIN_EMAILS),
4241
allowedLoginDomains: parseCsv(env.ALLOWED_LOGIN_DOMAINS),
4342
blockedEmailMxHosts: parseCsv(env.BLOCKED_EMAIL_MX_HOSTS),
44-
bannedEmails: [],
4543
}
4644
}
4745

@@ -52,26 +50,16 @@ function parseConfig(json: unknown): AccessControlConfig {
5250
allowedLoginEmails: normalizeList(obj.allowedLoginEmails),
5351
allowedLoginDomains: normalizeList(obj.allowedLoginDomains),
5452
blockedEmailMxHosts: normalizeList(obj.blockedEmailMxHosts),
55-
bannedEmails: normalizeList(obj.bannedEmails),
5653
}
5754
}
5855

5956
/**
60-
* AppConfig is the source of truth only when both identifiers are present
61-
* (injected by the infra stack). Mirrors the `hasS3Config` presence-check
62-
* pattern — the AppConfig client is never constructed otherwise.
63-
*/
64-
function isAppConfigEnabled(): boolean {
65-
return Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT)
66-
}
67-
68-
/**
69-
* Resolve the current signup/login gating lists. Reads from AWS AppConfig when
70-
* configured (cached, ~30s TTL, never blocks after the first fetch), otherwise
71-
* falls back to env vars so self-hosted/OSS deployments work with no AWS.
57+
* Resolve the current signup/login gating lists. Reads from AWS AppConfig on
58+
* hosted deployments (cached, ~30s TTL, never blocks after the first fetch),
59+
* otherwise falls back to env vars so self-hosted/OSS works with no AWS.
7260
*/
7361
export async function getAccessControlConfig(): Promise<AccessControlConfig> {
74-
if (!isAppConfigEnabled()) return fromEnv()
62+
if (!isAppConfigEnabled) return fromEnv()
7563

7664
const value = await fetchAppConfigProfile(
7765
{

apps/sim/lib/auth/auth.ts

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,6 @@ export const auth = betterAuth({
234234
create: {
235235
before: async (user) => {
236236
const accessControl = await getAccessControlConfig()
237-
const email = user.email?.toLowerCase()
238-
if (email && accessControl.bannedEmails.includes(email)) {
239-
throw new Error('Sign-ups from this email domain are not allowed.')
240-
}
241237
if (isEmailInDenylist(user.email, accessControl.blockedSignupDomains)) {
242238
throw new Error('Sign-ups from this email domain are not allowed.')
243239
}
@@ -812,10 +808,9 @@ export const auth = betterAuth({
812808
const accessControl = await getAccessControlConfig()
813809
const requestEmail = ctx.body?.email?.toLowerCase()
814810

815-
// Note: banning an existing account is owned by better-auth's admin plugin
816-
// (a `session.create.before` hook that blocks banned users at sign-in across
817-
// all providers). `bannedEmails` here is a signup-only denylist enforced in
818-
// `databaseHooks.user.create.before`, so it is not checked on sign-in.
811+
// Banning an existing account is owned by better-auth's admin plugin (a
812+
// `session.create.before` hook that blocks banned users at sign-in across
813+
// all providers), so it is not re-checked here.
819814
const hasAllowlist =
820815
accessControl.allowedLoginEmails.length > 0 ||
821816
accessControl.allowedLoginDomains.length > 0

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export const env = createEnv({
223223
S3_FORCE_PATH_STYLE: z.string().optional(), // Force path-style addressing (MinIO/Ceph RGW). Defaults to false (AWS S3, R2). Coerced via envBoolean at the consumption site
224224

225225
// Dynamic config - AWS AppConfig (hosted source of truth for signup/login gating lists; unset => env-var fallback)
226-
APPCONFIG_APPLICATION: z.string().optional(), // AppConfig application id/name. When set with APPCONFIG_ENVIRONMENT, gating lists come from AppConfig instead of env vars
226+
APPCONFIG_APPLICATION: z.string().optional(), // AppConfig application id/name. On hosted deployments, when set with APPCONFIG_ENVIRONMENT, gating lists come from AppConfig instead of env vars
227227
APPCONFIG_ENVIRONMENT: z.string().optional(), // AppConfig environment id/name. Profile name is an app-side constant ('access-control'), not an env var
228228

229229
// Cloud Storage - Azure Blob

apps/sim/lib/core/config/feature-flags.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ export const isSignupEmailValidationEnabled = isTruthy(env.SIGNUP_EMAIL_VALIDATI
9696
*/
9797
export const isSignupMxValidationEnabled = isTruthy(env.SIGNUP_MX_VALIDATION_ENABLED)
9898

99+
/**
100+
* Is AWS AppConfig the source of truth for the signup/login gating lists.
101+
* Hosted-only and requires both AppConfig identifiers (injected by the infra
102+
* stack). Self-hosted/OSS deployments always use the env-var fallback, so the
103+
* AppConfig client is never reached off-hosted.
104+
*/
105+
export const isAppConfigEnabled =
106+
isHosted && Boolean(env.APPCONFIG_APPLICATION && env.APPCONFIG_ENVIRONMENT)
107+
99108
/**
100109
* Is Trigger.dev enabled for async job processing
101110
*/

0 commit comments

Comments
 (0)