Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ AUTH_URL="http://localhost:3000"
# AUTH_EE_GOOGLE_CLIENT_ID=""
# AUTH_EE_GOOGLE_CLIENT_SECRET=""

# FORCE_ENABLE_ANONYMOUS_ACCESS="false"
# FORCE_MEMBER_APPROVAL_REQUIRED="false"

DATA_CACHE_DIR=${PWD}/.sourcebot # Path to the sourcebot cache dir (ex. ~/sourcebot/.sourcebot)
SOURCEBOT_PUBLIC_KEY_PATH=${PWD}/public.pem
# CONFIG_PATH=${PWD}/config.json # Path to the sourcebot config file (if one exists)
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
- Enable configuration of member approval via Env Var. [#542](https://github.com/sourcebot-dev/sourcebot/pull/542)

## [4.7.2] - 2025-09-22

Expand Down
1 change: 1 addition & 0 deletions docs/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following environment variables allow you to configure your Sourcebot deploy
| `DATABASE_URL` | `postgresql://postgres@ localhost:5432/sourcebot` | <p>Connection string of your Postgres database. By default, a Postgres database is automatically provisioned at startup within the container.</p><p>If you'd like to use a non-default schema, you can provide it as a parameter in the database url </p> |
| `EMAIL_FROM_ADDRESS` | `-` | <p>The email address that transactional emails will be sent from. See [this doc](/docs/configuration/transactional-emails) for more info.</p> |
| `FORCE_ENABLE_ANONYMOUS_ACCESS` | `false` | <p>When enabled, [anonymous access](/docs/configuration/auth/access-settings#anonymous-access) to the organization will always be enabled</p>
| `FORCE_MEMBER_APPROVAL_REQUIRED` | `-` | <p>When set to `true` or `false`, forces the member approval requirement setting and disables the UI toggle. When enabled, new users will need approval from an organization owner before they can access your deployment. See [access settings docs](/docs/configuration/auth/access-settings) for more info</p>
| `REDIS_DATA_DIR` | `$DATA_CACHE_DIR/redis` | <p>The data directory for the default Redis instance.</p> |
| `REDIS_URL` | `redis://localhost:6379` | <p>Connection string of your Redis instance. By default, a Redis database is automatically provisioned at startup within the container.</p> |
| `REDIS_REMOVE_ON_COMPLETE` | `0` | <p>Controls how many completed jobs are allowed to remain in Redis queues</p> |
Expand Down
30 changes: 28 additions & 2 deletions packages/web/src/app/components/memberApprovalRequiredToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import { useToast } from "@/components/hooks/use-toast"
interface MemberApprovalRequiredToggleProps {
memberApprovalRequired: boolean
onToggleChange?: (checked: boolean) => void
forceMemberApprovalRequired?: string
}

export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange }: MemberApprovalRequiredToggleProps) {
export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleChange, forceMemberApprovalRequired }: MemberApprovalRequiredToggleProps) {
const [enabled, setEnabled] = useState(memberApprovalRequired)
const [isLoading, setIsLoading] = useState(false)
const { toast } = useToast()
Expand Down Expand Up @@ -45,6 +46,9 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
}
}

const isDisabled = isLoading || forceMemberApprovalRequired !== undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to forceEnableAnonymousAccess we only need to disable if the forceMemberApprovalRequired flag is true

const showForceMessage = forceMemberApprovalRequired !== undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to the forceEnableAnonymousAccess flag, we probably only want to show this message if forceMemberApprovalRequired is true


return (
<div className="p-4 rounded-lg border border-[var(--border)] bg-[var(--card)]">
<div className="flex items-start justify-between gap-4">
Expand All @@ -56,13 +60,35 @@ export function MemberApprovalRequiredToggle({ memberApprovalRequired, onToggleC
<p className="text-sm text-[var(--muted-foreground)] leading-relaxed">
When enabled, new users will need approval from an organization owner before they can access your deployment.
</p>
{showForceMessage && (
<div className="mt-3">
<p className="flex items-start gap-2 text-sm text-[var(--muted-foreground)] p-3 rounded-md bg-[var(--muted)] border border-[var(--border)]">
<svg
className="w-4 h-4 mt-0.5 flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>
The <code className="bg-[var(--secondary)] px-1 py-0.5 rounded text-xs font-mono">FORCE_MEMBER_APPROVAL_REQUIRED</code> environment variable is set, so this cannot be changed from the UI.
</span>
</p>
</div>
)}
</div>
</div>
<div className="flex-shrink-0">
<Switch
checked={enabled}
onCheckedChange={handleToggle}
disabled={isLoading}
disabled={isDisabled}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export async function OrganizationAccessSettings() {
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");

const forceEnableAnonymousAccess = env.FORCE_ENABLE_ANONYMOUS_ACCESS === 'true';
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Normalize FORCE_MEMBER_APPROVAL_REQUIRED to boolean|undefined before passing to UI.

Keeps semantics clear (enforced true/false vs. not enforced) and aligns with forceEnableAnonymousAccess.

-    const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+    const forceMemberApprovalRequired = (() => {
+        const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+        if (v === "true") return true;
+        if (v === "false") return false;
+        return undefined;
+    })();

Run to confirm there are no other string-typed usages that would conflict:

Also applies to: 41-41


🏁 Script executed:

#!/bin/bash
# Verify all references and ensure downstream types can accept boolean|undefined
rg -n --pretty --no-ignore 'FORCE_MEMBER_APPROVAL_REQUIRED'
rg -n --type=ts -C2 'forceMemberApprovalRequired\??:\s*string'

Length of output: 3006


Normalize FORCE_MEMBER_APPROVAL_REQUIRED to boolean|undefined across UI components

  • In organizationAccessSettings.tsx, coerce the raw env string to boolean | undefined:
- const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
+ const forceMemberApprovalRequired = (() => {
+   const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
+   if (v === 'true') return true;
+   if (v === 'false') return false;
+   return undefined;
+ })();
  • Update prop types to match in organizationAccessSettingsWrapper.tsx and memberApprovalRequiredToggle.tsx:
-  forceMemberApprovalRequired?: string
+  forceMemberApprovalRequired?: boolean
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED;
// before: raw string from env
const forceMemberApprovalRequired = (() => {
const v = env.FORCE_MEMBER_APPROVAL_REQUIRED?.toLowerCase();
if (v === 'true') return true;
if (v === 'false') return false;
return undefined;
})();
🤖 Prompt for AI Agents
In packages/web/src/app/components/organizationAccessSettings.tsx around line
27, env.FORCE_MEMBER_APPROVAL_REQUIRED is currently a raw string; coerce it to
boolean | undefined (e.g., if value === "true" return true, if value === "false"
return false, otherwise undefined). Then update the prop types in
packages/web/src/app/components/organizationAccessSettingsWrapper.tsx and
packages/web/src/app/components/memberApprovalRequiredToggle.tsx to accept
boolean | undefined and ensure all places that pass this value forward use the
normalized boolean/undefined rather than the raw env string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

coderabbit is probably right here although its suggestion is verbose. env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' should be fine

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@msukkari You're absolutely right! The simpler approach is much cleaner and follows the same pattern as forceEnableAnonymousAccess. Here's the updated suggestion:

const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';

This maintains consistency with the existing forceEnableAnonymousAccess pattern and is much more readable. We'll also need to update the prop types in the downstream components from string to boolean to match this change.


🏁 Script executed:

#!/bin/bash
# Check the prop types for forceMemberApprovalRequired in the downstream components
rg -n --type=ts -A5 -B5 'forceMemberApprovalRequired.*:.*string' packages/web/src/app/components/

Length of output: 2353


Perfect! Now I can see the complete picture. Here are the changes needed across all three components to implement the simpler boolean approach:

1. In packages/web/src/app/components/organizationAccessSettings.tsx:

const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';

2. In packages/web/src/app/components/organizationAccessSettingsWrapper.tsx:

interface OrganizationAccessSettingsWrapperProps {
    memberApprovalRequired: boolean
    inviteLinkEnabled: boolean
    inviteLink: string | null
    forceMemberApprovalRequired?: boolean  // Changed from string to boolean
}

3. In packages/web/src/app/components/memberApprovalRequiredToggle.tsx:

interface MemberApprovalRequiredToggleProps {
    memberApprovalRequired: boolean
    onToggleChange?: (checked: boolean) => void
    forceMemberApprovalRequired?: boolean  // Changed from string to boolean
}

This approach is much cleaner, follows the established pattern with forceEnableAnonymousAccess, and maintains type safety throughout the component chain. The boolean value will work perfectly with the UI logic that checks forceMemberApprovalRequired !== undefined to determine if the toggle should be disabled.


return (
<div className="space-y-6">
Expand All @@ -37,6 +38,7 @@ export async function OrganizationAccessSettings() {
memberApprovalRequired={org.memberApprovalRequired}
inviteLinkEnabled={org.inviteLinkEnabled}
inviteLink={inviteLink}
forceMemberApprovalRequired={forceMemberApprovalRequired}
/>
</div>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ interface OrganizationAccessSettingsWrapperProps {
memberApprovalRequired: boolean
inviteLinkEnabled: boolean
inviteLink: string | null
forceMemberApprovalRequired?: string
}

export function OrganizationAccessSettingsWrapper({
memberApprovalRequired,
inviteLinkEnabled,
inviteLink
inviteLink,
forceMemberApprovalRequired
}: OrganizationAccessSettingsWrapperProps) {
const [showInviteLink, setShowInviteLink] = useState(memberApprovalRequired)

Expand All @@ -27,6 +29,7 @@ export function OrganizationAccessSettingsWrapper({
<MemberApprovalRequiredToggle
memberApprovalRequired={memberApprovalRequired}
onToggleChange={handleMemberApprovalToggle}
forceMemberApprovalRequired={forceMemberApprovalRequired}
/>
</div>

Expand Down
1 change: 1 addition & 0 deletions packages/web/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const env = createEnv({

// Auth
FORCE_ENABLE_ANONYMOUS_ACCESS: booleanSchema.default('false'),
FORCE_MEMBER_APPROVAL_REQUIRED: booleanSchema.optional(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

following the rest of the changes to only force UI stuff when this is enabled, we can also default this to false to match the other env var


AUTH_SECRET: z.string(),
AUTH_URL: z.string().url(),
Expand Down
29 changes: 29 additions & 0 deletions packages/web/src/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ const syncDeclarativeConfig = async (configPath: string) => {
}
}

// Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (org) {
await prisma.org.update({
where: { id: org.id },
data: { memberApprovalRequired: forceMemberApprovalRequired },
});
logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
}
}
Comment on lines +134 to +145
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Sep 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Force must be enforced server‑side, not only at init

Currently this sets the value on startup/sync, but an API call could still flip memberApprovalRequired if the UI is bypassed. Add a guard in the write path (e.g., access‑settings update handler) to reject changes when FORCE_MEMBER_APPROVAL_REQUIRED is defined.

Run to locate and patch the update path(s):

Suggested server check (illustrative):

// In the handler that updates org access settings:
import { env } from '@/env.mjs';

if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
  return new Response(JSON.stringify({ error: 'memberApprovalRequired is forced by env' }), { status: 409 });
}

🏁 Script executed:

#!/bin/bash
# Find all updates to memberApprovalRequired and the access settings API.
rg -nP -C3 '\bmemberApprovalRequired\s*:' --type=ts
rg -nP -C3 '(access|org).*settings|memberApproval' --type=ts -g '!**/node_modules/**'

Length of output: 4482


Enforce FORCE_MEMBER_APPROVAL_REQUIRED in org update endpoint (actions.ts:1816–1822)

  • Add a guard around the prisma.org.update call in packages/web/src/actions.ts (lines 1816–1822) to return a 409 error if env.FORCE_MEMBER_APPROVAL_REQUIRED is set.
  • Extract the override logic into a shared helper and skip the database update when the existing value already matches the enforced flag.
🤖 Prompt for AI Agents
In packages/web/src/initialize.ts around lines 134–145, the code unconditionally
updates org.memberApprovalRequired when FORCE_MEMBER_APPROVAL_REQUIRED is set;
extract this override logic into a shared helper (e.g., services/orgOverride.ts)
that accepts the enforced boolean and the org id and only performs a
prisma.org.update when the current DB value differs, and update initialize.ts to
call that helper instead of doing the update inline; then in
packages/web/src/actions.ts around lines 1816–1822, add a guard that checks
process.env.FORCE_MEMBER_APPROVAL_REQUIRED and if present returns a 409 response
for any request attempting to change memberApprovalRequired (using the same
shared helper to determine the enforced value), thereby preventing DB changes
and avoiding redundant updates when the current value already matches the
enforced flag.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not super needed since the only way we set this is in the UI and we're properly disabling it when the env var is enabeld

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

@msukkari I understand your point about the UI being disabled, but I'd like to verify the API surface to ensure there aren't other ways to bypass this protection.


🏁 Script executed:

#!/bin/bash
# Find API routes and actions that can modify memberApprovalRequired
rg -nP -C5 'memberApprovalRequired.*=|memberApprovalRequired.*:' --type=ts -g '!**/node_modules/**'

# Look for API endpoints or server actions related to org settings
rg -nP -C3 'export.*action|export.*POST|export.*PUT' --type=ts -g '**/actions.ts' -g '**/api/**'

# Check for any direct prisma updates to org.memberApprovalRequired
rg -nP -C3 'prisma\.org\.update.*memberApprovalRequired|memberApprovalRequired.*prisma\.org\.update' --type=ts

Length of output: 3596


🏁 Script executed:

#!/bin/bash
# Find the actions.ts file and any org update functionality
fd -t f "actions.ts" --exec rg -nP -C5 'memberApprovalRequired' {} \;

# Look for org update server actions more broadly
rg -nP -C3 'org.*update|update.*org' --type=ts -g '!**/node_modules/**'

# Search for any prisma org updates
rg -nP -C3 'prisma\.org\.update' --type=ts -g '!**/node_modules/**'

Length of output: 1422


@msukkari I found the server action that can bypass the UI protection. Looking at the code, there's a setMemberApprovalRequired function that directly updates the database without checking the environment variable:

// From actions.ts lines 1814-1824
export const setMemberApprovalRequired = async (domain: string, required: boolean): Promise<{ success: boolean } | ServiceError> => sew(async () =>
    withAuth(async (userId) =>
        withOrgMembership(userId, domain, async ({ org }) => {
            await prisma.org.update({
                where: { id: org.id },
                data: { memberApprovalRequired: required },
            });

            return {
                success: true,
            };

This server action can be called directly via form actions or fetch requests, completely bypassing the disabled UI toggle. While the UI protection is good UX, relying solely on client-side controls for enforcement isn't secure.

A simple server-side check in this function would ensure the environment variable setting is truly enforced:

if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
    return { statusCode: 409, errorCode: "SETTING_FORCED_BY_ENV", message: "Member approval setting is controlled by environment variable" };
}

This would provide defense in depth and ensure the configuration can't be circumvented.


await syncConnections(config.connections);
await syncSearchContexts({
contexts: config.contexts,
Expand Down Expand Up @@ -180,6 +193,9 @@ const initSingleTenancy = async () => {
name: SINGLE_TENANT_ORG_NAME,
domain: SINGLE_TENANT_ORG_DOMAIN,
inviteLinkId: crypto.randomUUID(),
memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true :
env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false :
true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
Comment on lines +196 to +198
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
memberApprovalRequired: env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' ? true :
env.FORCE_MEMBER_APPROVAL_REQUIRED === 'false' ? false :
true, // default to true if FORCE_MEMBER_APPROVAL_REQUIRED is not set
...(env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true' && {
memberApprovalRequired: true,
})

default values for data in the db are set by the schema so its best to not have them set in the code. We only want to set this if the env var is true anyways

}
});
} else if (!org.inviteLinkId) {
Expand Down Expand Up @@ -220,6 +236,19 @@ const initSingleTenancy = async () => {
}
}

// Apply FORCE_MEMBER_APPROVAL_REQUIRED environment variable setting
if (env.FORCE_MEMBER_APPROVAL_REQUIRED !== undefined) {
const forceMemberApprovalRequired = env.FORCE_MEMBER_APPROVAL_REQUIRED === 'true';
const org = await getOrgFromDomain(SINGLE_TENANT_ORG_DOMAIN);
if (org) {
await prisma.org.update({
where: { id: org.id },
data: { memberApprovalRequired: forceMemberApprovalRequired },
});
logger.info(`Member approval required set to ${forceMemberApprovalRequired} via FORCE_MEMBER_APPROVAL_REQUIRED environment variable`);
}
}

// Load any connections defined declaratively in the config file.
const configPath = env.CONFIG_PATH;
if (configPath) {
Expand Down
Loading