diff --git a/LICENSE.md b/LICENSE.md index 93c14254..315bde81 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,7 +2,7 @@ Copyright (c) 2025 Taqla Inc. Portions of this software are licensed as follows: -- All content that resides under the "ee/", "packages/web/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE". +- All content that resides under the "ee/", "packages/web/src/ee/", "packages/backend/src/ee/", and "packages/shared/src/ee/" directories of this repository, if these directories exist, is licensed under the license defined in "ee/LICENSE". - All third party components incorporated into the Sourcebot Software are licensed under the original license provided by the owner of the applicable component. - Content outside of the above mentioned directories or restrictions above is available under the "Functional Source License" as defined below. diff --git a/packages/backend/src/repoPermissionSyncer.ts b/packages/backend/src/ee/repoPermissionSyncer.ts similarity index 96% rename from packages/backend/src/repoPermissionSyncer.ts rename to packages/backend/src/ee/repoPermissionSyncer.ts index b6d8be8d..e4bec4c4 100644 --- a/packages/backend/src/repoPermissionSyncer.ts +++ b/packages/backend/src/ee/repoPermissionSyncer.ts @@ -7,10 +7,11 @@ import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"; import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"; import { Job, Queue, Worker } from 'bullmq'; import { Redis } from 'ioredis'; -import { env } from "./env.js"; -import { createOctokitFromConfig, getUserIdsWithReadAccessToRepo } from "./github.js"; -import { RepoWithConnections } from "./types.js"; -import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "./constants.js"; +import { env } from "../env.js"; +import { createOctokitFromConfig, getUserIdsWithReadAccessToRepo } from "../github.js"; +import { RepoWithConnections } from "../types.js"; +import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js"; +import { hasEntitlement } from "@sourcebot/shared"; type RepoPermissionSyncJob = { jobId: string; @@ -41,6 +42,10 @@ export class RepoPermissionSyncer { } public startScheduler() { + if (!hasEntitlement('permission-syncing')) { + throw new Error('Permission syncing is not supported in current plan.'); + } + logger.debug('Starting scheduler'); return setInterval(async () => { diff --git a/packages/backend/src/userPermissionSyncer.ts b/packages/backend/src/ee/userPermissionSyncer.ts similarity index 95% rename from packages/backend/src/userPermissionSyncer.ts rename to packages/backend/src/ee/userPermissionSyncer.ts index 872b1f7a..f8b39f45 100644 --- a/packages/backend/src/userPermissionSyncer.ts +++ b/packages/backend/src/ee/userPermissionSyncer.ts @@ -4,9 +4,10 @@ import { PrismaClient, User, UserPermissionSyncJobStatus } from "@sourcebot/db"; import { createLogger } from "@sourcebot/logger"; import { Job, Queue, Worker } from "bullmq"; import { Redis } from "ioredis"; -import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "./constants.js"; -import { env } from "./env.js"; -import { getReposThatAuthenticatedUserHasReadAccessTo } from "./github.js"; +import { PERMISSION_SYNC_SUPPORTED_CODE_HOST_TYPES } from "../constants.js"; +import { env } from "../env.js"; +import { getReposThatAuthenticatedUserHasReadAccessTo } from "../github.js"; +import { hasEntitlement } from "@sourcebot/shared"; const logger = createLogger('user-permission-syncer'); @@ -37,6 +38,10 @@ export class UserPermissionSyncer { } public startScheduler() { + if (!hasEntitlement('permission-syncing')) { + throw new Error('Permission syncing is not supported in current plan.'); + } + logger.debug('Starting scheduler'); return setInterval(async () => { diff --git a/packages/backend/src/env.ts b/packages/backend/src/env.ts index 5d056284..4715b635 100644 --- a/packages/backend/src/env.ts +++ b/packages/backend/src/env.ts @@ -53,7 +53,7 @@ export const env = createEnv({ GITLAB_CLIENT_QUERY_TIMEOUT_SECONDS: numberSchema.default(60 * 10), - EXPERIMENT_PERMISSION_SYNC_ENABLED: booleanSchema.default("false"), + EXPERIMENT_EE_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), }, runtimeEnv: process.env, emptyStringAsUndefined: true, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0210b3ba..2182fbad 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -2,7 +2,7 @@ import "./instrument.js"; import { PrismaClient } from "@sourcebot/db"; import { createLogger } from "@sourcebot/logger"; -import { loadConfig } from '@sourcebot/shared'; +import { hasEntitlement, loadConfig } from '@sourcebot/shared'; import { existsSync } from 'fs'; import { mkdir } from 'fs/promises'; import { Redis } from 'ioredis'; @@ -10,11 +10,11 @@ import path from 'path'; import { ConnectionManager } from './connectionManager.js'; import { DEFAULT_SETTINGS } from './constants.js'; import { env } from "./env.js"; -import { RepoPermissionSyncer } from './repoPermissionSyncer.js'; +import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js'; import { PromClient } from './promClient.js'; import { RepoManager } from './repoManager.js'; import { AppContext } from "./types.js"; -import { UserPermissionSyncer } from "./userPermissionSyncer.js"; +import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js"; const logger = createLogger('backend-entrypoint'); @@ -76,9 +76,18 @@ await repoManager.validateIndexedReposHaveShards(); const connectionManagerInterval = connectionManager.startScheduler(); const repoManagerInterval = repoManager.startScheduler(); -const repoPermissionSyncerInterval = env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? repoPermissionSyncer.startScheduler() : null; -const userPermissionSyncerInterval = env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? userPermissionSyncer.startScheduler() : null; +let repoPermissionSyncerInterval: NodeJS.Timeout | null = null; +let userPermissionSyncerInterval: NodeJS.Timeout | null = null; + +if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('permission-syncing')) { + logger.error('Permission syncing is not supported in current plan. Please contact support@sourcebot.dev for assistance.'); + process.exit(1); +} +else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) { + repoPermissionSyncerInterval = repoPermissionSyncer.startScheduler(); + userPermissionSyncerInterval = userPermissionSyncer.startScheduler(); +} const cleanup = async (signal: string) => { logger.info(`Recieved ${signal}, cleaning up...`); diff --git a/packages/shared/src/entitlements.ts b/packages/shared/src/entitlements.ts index 965989c1..be40b927 100644 --- a/packages/shared/src/entitlements.ts +++ b/packages/shared/src/entitlements.ts @@ -38,15 +38,16 @@ const entitlements = [ "sso", "code-nav", "audit", - "analytics" + "analytics", + "permission-syncing" ] as const; export type Entitlement = (typeof entitlements)[number]; const entitlementsByPlan: Record = { oss: ["anonymous-access"], "cloud:team": ["billing", "multi-tenancy", "sso", "code-nav"], - "self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics"], - "self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics"], + "self-hosted:enterprise": ["search-contexts", "sso", "code-nav", "audit", "analytics", "permission-syncing"], + "self-hosted:enterprise-unlimited": ["search-contexts", "anonymous-access", "sso", "code-nav", "audit", "analytics", "permission-syncing"], // Special entitlement for https://demo.sourcebot.dev "cloud:demo": ["anonymous-access", "code-nav", "search-contexts"], } as const; diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 215ebef6..77301c77 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -1035,7 +1035,7 @@ export const flagReposForIndex = async (repoIds: number[], domain: string) => se where: { id: { in: repoIds }, orgId: org.id, - ...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? { + ...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' ? { permittedUsers: { some: { userId: userId, diff --git a/packages/web/src/ee/features/sso/sso.tsx b/packages/web/src/ee/features/sso/sso.tsx index 07332b63..0f14a364 100644 --- a/packages/web/src/ee/features/sso/sso.tsx +++ b/packages/web/src/ee/features/sso/sso.tsx @@ -12,6 +12,7 @@ import Credentials from "next-auth/providers/credentials"; import type { User as AuthJsUser } from "next-auth"; import { onCreateUser } from "@/lib/authUtils"; import { createLogger } from "@sourcebot/logger"; +import { hasEntitlement } from "@sourcebot/shared"; const logger = createLogger('web-sso'); @@ -30,10 +31,10 @@ export const getSSOProviders = (): Provider[] => { scope: [ 'read:user', 'user:email', - // Permission syncing requires the `repo` in order to fetch repositories + // Permission syncing requires the `repo` scope in order to fetch repositories // for the authenticated user. // @see: https://docs.github.com/en/rest/repos/repos?apiVersion=2022-11-28#list-repositories-for-the-authenticated-user - ...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? + ...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? ['repo'] : [] ), diff --git a/packages/web/src/env.mjs b/packages/web/src/env.mjs index b6fcb6ff..922b2b84 100644 --- a/packages/web/src/env.mjs +++ b/packages/web/src/env.mjs @@ -137,7 +137,7 @@ export const env = createEnv({ // @NOTE: Take care to update actions.ts when changing the name of this. EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN: z.string().optional(), - EXPERIMENT_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), + EXPERIMENT_EE_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'), }, // @NOTE: Please make sure of the following: // - Make sure you destructure all client variables in diff --git a/packages/web/src/prisma.ts b/packages/web/src/prisma.ts index 5904c522..1908c5fb 100644 --- a/packages/web/src/prisma.ts +++ b/packages/web/src/prisma.ts @@ -1,6 +1,7 @@ import 'server-only'; import { env } from "@/env.mjs"; import { Prisma, PrismaClient } from "@sourcebot/db"; +import { hasEntitlement } from "@sourcebot/shared"; // @see: https://authjs.dev/getting-started/adapters/prisma const globalForPrisma = globalThis as unknown as { prisma: PrismaClient } @@ -24,7 +25,7 @@ export const userScopedPrismaClientExtension = (userId?: string) => { (prisma) => { return prisma.$extends({ query: { - ...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? { + ...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? { repo: { $allOperations({ args, query }) { if ('where' in args) {