Skip to content

Commit 7f288c3

Browse files
committed
migrate anonymous access logic out of ee
1 parent 55c8e41 commit 7f288c3

File tree

21 files changed

+165
-232
lines changed

21 files changed

+165
-232
lines changed

demo-site-config.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,6 @@
238238
}
239239
},
240240
"settings": {
241-
"reindexIntervalMs": 86400000, // 24 hours
242-
"enablePublicAccess": true
241+
"reindexIntervalMs": 86400000 // 24 hours
243242
}
244243
}

docs/snippets/schemas/v3/index.schema.mdx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@
6363
"type": "number",
6464
"description": "The timeout (in milliseconds) for a repo indexing to timeout. Defaults to 2 hours.",
6565
"minimum": 1
66-
},
67-
"enablePublicAccess": {
68-
"type": "boolean",
69-
"description": "[Sourcebot EE] When enabled, allows unauthenticated users to access Sourcebot. Requires an enterprise license with an unlimited number of seats.",
70-
"default": false
7166
}
7267
},
7368
"additionalProperties": false
@@ -177,11 +172,6 @@
177172
"type": "number",
178173
"description": "The timeout (in milliseconds) for a repo indexing to timeout. Defaults to 2 hours.",
179174
"minimum": 1
180-
},
181-
"enablePublicAccess": {
182-
"type": "boolean",
183-
"description": "[Sourcebot EE] When enabled, allows unauthenticated users to access Sourcebot. Requires an enterprise license with an unlimited number of seats.",
184-
"default": false
185175
}
186176
},
187177
"additionalProperties": false

packages/backend/src/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,4 @@ export const DEFAULT_SETTINGS: Settings = {
1515
maxRepoGarbageCollectionJobConcurrency: 8,
1616
repoGarbageCollectionGracePeriodMs: 10 * 1000, // 10 seconds
1717
repoIndexTimeoutMs: 1000 * 60 * 60 * 2, // 2 hours
18-
enablePublicAccess: false,
1918
}

packages/schemas/src/v2/index.schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2258,4 +2258,4 @@ const schema = {
22582258
},
22592259
"additionalProperties": false
22602260
} as const;
2261-
export { schema as indexSchema };
2261+
export { schema as indexSchema };

packages/schemas/src/v3/index.schema.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ const schema = {
6262
"type": "number",
6363
"description": "The timeout (in milliseconds) for a repo indexing to timeout. Defaults to 2 hours.",
6464
"minimum": 1
65-
},
66-
"enablePublicAccess": {
67-
"type": "boolean",
68-
"description": "[Sourcebot EE] When enabled, allows unauthenticated users to access Sourcebot. Requires an enterprise license with an unlimited number of seats.",
69-
"default": false
7065
}
7166
},
7267
"additionalProperties": false
@@ -176,11 +171,6 @@ const schema = {
176171
"type": "number",
177172
"description": "The timeout (in milliseconds) for a repo indexing to timeout. Defaults to 2 hours.",
178173
"minimum": 1
179-
},
180-
"enablePublicAccess": {
181-
"type": "boolean",
182-
"description": "[Sourcebot EE] When enabled, allows unauthenticated users to access Sourcebot. Requires an enterprise license with an unlimited number of seats.",
183-
"default": false
184174
}
185175
},
186176
"additionalProperties": false

packages/schemas/src/v3/index.type.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,6 @@ export interface Settings {
7979
* The timeout (in milliseconds) for a repo indexing to timeout. Defaults to 2 hours.
8080
*/
8181
repoIndexTimeoutMs?: number;
82-
/**
83-
* [Sourcebot EE] When enabled, allows unauthenticated users to access Sourcebot. Requires an enterprise license with an unlimited number of seats.
84-
*/
85-
enablePublicAccess?: boolean;
8682
}
8783
/**
8884
* Search context

packages/shared/src/entitlements.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export type Plan = keyof typeof planLabels;
3333
const entitlements = [
3434
"search-contexts",
3535
"billing",
36-
"public-access",
36+
"anonymous-access",
3737
"multi-tenancy",
3838
"sso",
3939
"code-nav",
@@ -43,12 +43,12 @@ const entitlements = [
4343
export type Entitlement = (typeof entitlements)[number];
4444

4545
const entitlementsByPlan: Record<Plan, Entitlement[]> = {
46-
oss: [],
46+
oss: ["anonymous-access"],
4747
"cloud:team": ["billing", "multi-tenancy", "sso", "code-nav"],
4848
"self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics"],
49-
"self-hosted:enterprise-unlimited": ["search-contexts", "public-access", "sso", "code-nav", "audit", "analytics"],
49+
"self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics"],
5050
// Special entitlement for https://demo.sourcebot.dev
51-
"cloud:demo": ["public-access", "code-nav", "search-contexts"],
51+
"cloud:demo": ["anonymous-access", "code-nav", "search-contexts"],
5252
} as const;
5353

5454

packages/web/src/actions.ts

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,13 @@ import { decrementOrgSeatCount, getSubscriptionForOrg } from "./ee/features/bill
3232
import { bitbucketSchema } from "@sourcebot/schemas/v3/bitbucket.schema";
3333
import { genericGitHostSchema } from "@sourcebot/schemas/v3/genericGitHost.schema";
3434
import { getPlan, hasEntitlement } from "@sourcebot/shared";
35-
import { getPublicAccessStatus } from "./ee/features/publicAccess/publicAccess";
3635
import JoinRequestSubmittedEmail from "./emails/joinRequestSubmittedEmail";
3736
import JoinRequestApprovedEmail from "./emails/joinRequestApprovedEmail";
3837
import { createLogger } from "@sourcebot/logger";
3938
import { getAuditService } from "@/ee/features/audit/factory";
4039
import { addUserToOrganization, orgHasAvailability } from "@/lib/authUtils";
40+
import { orgMetadataSchema } from "@/types";
41+
import { getOrgFromDomain } from "./data/org";
4142

4243
const ajv = new Ajv({
4344
validateFormats: false,
@@ -62,13 +63,13 @@ export const sew = async <T>(fn: () => Promise<T>): Promise<T | ServiceError> =>
6263
}
6364
}
6465

65-
export const withAuth = async <T>(fn: (userId: string, apiKeyHash: string | undefined) => Promise<T>, allowSingleTenantUnauthedAccess: boolean = false, apiKey: ApiKeyPayload | undefined = undefined) => {
66+
export const withAuth = async <T>(fn: (userId: string, apiKeyHash: string | undefined) => Promise<T>, allowAnonymousAccess: boolean = false, apiKey: ApiKeyPayload | undefined = undefined) => {
6667
const session = await auth();
6768

6869
if (!session) {
6970
// First we check if public access is enabled and supported. If not, then we check if an api key was provided. If not,
7071
// then this is an invalid unauthed request and we return a 401.
71-
const publicAccessEnabled = await getPublicAccessStatus(SINGLE_TENANT_ORG_DOMAIN);
72+
const anonymousAccessEnabled = await getAnonymousAccessStatus(SINGLE_TENANT_ORG_DOMAIN);
7273
if (apiKey) {
7374
const apiKeyOrError = await verifyApiKey(apiKey);
7475
if (isServiceError(apiKeyOrError)) {
@@ -98,18 +99,17 @@ export const withAuth = async <T>(fn: (userId: string, apiKeyHash: string | unde
9899

99100
return fn(user.id, apiKeyOrError.apiKey.hash);
100101
} else if (
101-
env.SOURCEBOT_TENANCY_MODE === 'single' &&
102-
allowSingleTenantUnauthedAccess &&
103-
!isServiceError(publicAccessEnabled) &&
104-
publicAccessEnabled
102+
allowAnonymousAccess &&
103+
!isServiceError(anonymousAccessEnabled) &&
104+
anonymousAccessEnabled
105105
) {
106-
if (!hasEntitlement("public-access")) {
106+
if (!hasEntitlement("anonymous-access")) {
107107
const plan = getPlan();
108-
logger.error(`Public access isn't supported in your current plan: ${plan}. If you have a valid enterprise license key, pass it via SOURCEBOT_EE_LICENSE_KEY. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
108+
logger.error(`Anonymous access isn't supported in your current plan: ${plan}. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
109109
return notAuthenticated();
110110
}
111111

112-
// To support unauthed access a guest user is created in initialize.ts, which we return here
112+
// To support anonymous access a guest user is created in initialize.ts, which we return here
113113
return fn(SOURCEBOT_GUEST_USER_ID, undefined);
114114
}
115115
return notAuthenticated();
@@ -672,7 +672,7 @@ export const getRepos = async (domain: string, filter: { status?: RepoIndexingSt
672672
indexedAt: repo.indexedAt ?? undefined,
673673
repoIndexingStatus: repo.repoIndexingStatus,
674674
}));
675-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
675+
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
676676
));
677677

678678
export const getRepoInfoByName = async (repoName: string, domain: string) => sew(() =>
@@ -734,7 +734,7 @@ export const getRepoInfoByName = async (repoName: string, domain: string) => sew
734734
indexedAt: repo.indexedAt ?? undefined,
735735
repoIndexingStatus: repo.repoIndexingStatus,
736736
}
737-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
737+
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
738738
));
739739

740740
export const createConnection = async (name: string, type: CodeHostType, connectionConfig: string, domain: string): Promise<{ id: number } | ServiceError> => sew(() =>
@@ -933,7 +933,7 @@ export const getCurrentUserRole = async (domain: string): Promise<OrgRole | Serv
933933
withAuth((userId) =>
934934
withOrgMembership(userId, domain, async ({ userRole }) => {
935935
return userRole;
936-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
936+
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
937937
));
938938

939939
export const createInvites = async (emails: string[], domain: string): Promise<{ success: boolean } | ServiceError> => sew(() =>
@@ -1863,7 +1863,7 @@ export const getSearchContexts = async (domain: string) => sew(() =>
18631863
name: context.name,
18641864
description: context.description ?? undefined,
18651865
}));
1866-
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowSingleTenantUnauthedAccess = */ true
1866+
}, /* minRequiredRole = */ OrgRole.GUEST), /* allowAnonymousAccess = */ true
18671867
));
18681868

18691869
export const getRepoImage = async (repoId: number, domain: string): Promise<ArrayBuffer | ServiceError> => sew(async () => {
@@ -1934,7 +1934,68 @@ export const getRepoImage = async (repoId: number, domain: string): Promise<Arra
19341934
return notFound();
19351935
}
19361936
}, /* minRequiredRole = */ OrgRole.GUEST);
1937-
}, /* allowSingleTenantUnauthedAccess = */ true);
1937+
}, /* allowAnonymousAccess = */ true);
1938+
});
1939+
1940+
export const getAnonymousAccessStatus = async (domain: string): Promise<boolean | ServiceError> => sew(async () => {
1941+
const org = await getOrgFromDomain(domain);
1942+
if (!org) {
1943+
return {
1944+
statusCode: StatusCodes.NOT_FOUND,
1945+
errorCode: ErrorCode.NOT_FOUND,
1946+
message: "Organization not found",
1947+
} satisfies ServiceError;
1948+
}
1949+
1950+
// If no metadata is set we don't try to parse it since it'll result in a parse error
1951+
if (org.metadata === null) {
1952+
return false;
1953+
}
1954+
1955+
const orgMetadata = orgMetadataSchema.safeParse(org.metadata);
1956+
if (!orgMetadata.success) {
1957+
return {
1958+
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
1959+
errorCode: ErrorCode.INVALID_ORG_METADATA,
1960+
message: "Invalid organization metadata",
1961+
} satisfies ServiceError;
1962+
}
1963+
1964+
return !!orgMetadata.data.anonymousAccessEnabled;
1965+
});
1966+
1967+
export const setAnonymousAccessStatus = async (domain: string, enabled: boolean): Promise<ServiceError | boolean> => sew(async () => {
1968+
return await withAuth(async (userId) => {
1969+
return await withOrgMembership(userId, domain, async ({ org }) => {
1970+
const hasAnonymousAccessEntitlement = hasEntitlement("anonymous-access");
1971+
if (!hasAnonymousAccessEntitlement) {
1972+
const plan = getPlan();
1973+
console.error(`Anonymous access isn't supported in your current plan: ${plan}. For support, contact ${SOURCEBOT_SUPPORT_EMAIL}.`);
1974+
return {
1975+
statusCode: StatusCodes.FORBIDDEN,
1976+
errorCode: ErrorCode.INSUFFICIENT_PERMISSIONS,
1977+
message: "Anonymous access is not supported in your current plan",
1978+
} satisfies ServiceError;
1979+
}
1980+
1981+
const currentMetadata = orgMetadataSchema.safeParse(org.metadata);
1982+
const mergedMetadata = {
1983+
...(currentMetadata.success ? currentMetadata.data : {}),
1984+
anonymousAccessEnabled: enabled,
1985+
};
1986+
1987+
await prisma.org.update({
1988+
where: {
1989+
id: org.id,
1990+
},
1991+
data: {
1992+
metadata: mergedMetadata,
1993+
},
1994+
});
1995+
1996+
return true;
1997+
}, /* minRequiredRole = */ OrgRole.OWNER);
1998+
});
19381999
});
19392000

19402001
////// Helpers ///////

packages/web/src/app/[domain]/layout.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ import { getSubscriptionInfo } from "@/ee/features/billing/actions";
1616
import { PendingApprovalCard } from "./components/pendingApproval";
1717
import { SubmitJoinRequest } from "./components/submitJoinRequest";
1818
import { hasEntitlement } from "@sourcebot/shared";
19-
import { getPublicAccessStatus } from "@/ee/features/publicAccess/publicAccess";
2019
import { env } from "@/env.mjs";
2120
import { GcpIapAuth } from "./components/gcpIapAuth";
22-
import { getMemberApprovalRequired } from "@/actions";
21+
import { getAnonymousAccessStatus, getMemberApprovalRequired } from "@/actions";
2322
import { JoinOrganizationCard } from "@/app/components/joinOrganizationCard";
2423
import { LogoutEscapeHatch } from "@/app/components/logoutEscapeHatch";
2524

@@ -39,7 +38,7 @@ export default async function Layout({
3938
}
4039

4140
const session = await auth();
42-
const publicAccessEnabled = hasEntitlement("public-access") && await getPublicAccessStatus(domain);
41+
const anonymousAccessEnabled = hasEntitlement("anonymous-access") && await getAnonymousAccessStatus(domain);
4342

4443
// If the user is authenticated, we must check if they're a member of the org
4544
if (session) {
@@ -84,8 +83,8 @@ export default async function Layout({
8483
}
8584
}
8685
} else {
87-
// If the user isn't authenticated and public access isn't enabled, we need to redirect them to the login page.
88-
if (!publicAccessEnabled) {
86+
// If the user isn't authenticated and anonymous access isn't enabled, we need to redirect them to the login page.
87+
if (!anonymousAccessEnabled) {
8988
const ssoEntitlement = await hasEntitlement("sso");
9089
if (ssoEntitlement && env.AUTH_EE_GCP_IAP_ENABLED && env.AUTH_EE_GCP_IAP_AUDIENCE) {
9190
return <GcpIapAuth callbackUrl={`/${domain}`} />;

packages/web/src/ee/features/analytics/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,5 @@ export const getAnalytics = async (domain: string, apiKey: string | undefined =
9999

100100

101101
return rows;
102-
}, /* minRequiredRole = */ OrgRole.MEMBER), /* allowSingleTenantUnauthedAccess = */ true, apiKey ? { apiKey, domain } : undefined)
102+
}, /* minRequiredRole = */ OrgRole.MEMBER), /* allowAnonymousAccess = */ true, apiKey ? { apiKey, domain } : undefined)
103103
);

0 commit comments

Comments
 (0)