Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed issue with an unbounded `Promise.allSettled(...)` when retrieving details from the GitHub API about a large number of repositories (or orgs or users). [#591](https://github.com/sourcebot-dev/sourcebot/pull/591)
- Fixed resource exhaustion (EAGAIN errors) when syncing generic-git-host connections with thousands of repositories. [#593](https://github.com/sourcebot-dev/sourcebot/pull/593)

## Removed
### Removed
- Removed built-in secret manager. [#592](https://github.com/sourcebot-dev/sourcebot/pull/592)

### Changed
- Changed internal representation of how repo permissions are represented in the database. [#600](https://github.com/sourcebot-dev/sourcebot/pull/600)

## [4.8.1] - 2025-10-29

### Fixed
Expand Down

Large diffs are not rendered by default.

20 changes: 7 additions & 13 deletions packages/backend/src/ee/repoPermissionSyncer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class RepoPermissionSyncer {
throw new Error(`No credentials found for repo ${id}`);
}

const userIds = await (async () => {
const accountIds = await (async () => {
if (repo.external_codeHostType === 'github') {
const isGitHubCloud = credentials.hostUrl ? new URL(credentials.hostUrl).hostname === GITHUB_CLOUD_HOSTNAME : false;
const { octokit } = await createOctokitFromToken({
Expand All @@ -195,12 +195,9 @@ export class RepoPermissionSyncer {
in: githubUserIds,
}
},
select: {
userId: true,
},
});

return accounts.map(account => account.userId);
return accounts.map(account => account.id);
} else if (repo.external_codeHostType === 'gitlab') {
const api = await createGitLabFromPersonalAccessToken({
token: credentials.token,
Expand All @@ -222,12 +219,9 @@ export class RepoPermissionSyncer {
in: gitlabUserIds,
}
},
select: {
userId: true,
},
});

return accounts.map(account => account.userId);
return accounts.map(account => account.id);
}

return [];
Expand All @@ -239,14 +233,14 @@ export class RepoPermissionSyncer {
id: repo.id,
},
data: {
permittedUsers: {
permittedAccounts: {
deleteMany: {},
}
}
}),
this.db.userToRepoPermission.createMany({
data: userIds.map(userId => ({
userId,
this.db.accountToRepoPermission.createMany({
data: accountIds.map(accountId => ({
accountId,
repoId: repo.id,
})),
})
Expand Down
8 changes: 4 additions & 4 deletions packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ConnectionManager } from './connectionManager.js';
import { INDEX_CACHE_DIR, REPOS_CACHE_DIR } from './constants.js';
import { GithubAppManager } from "./ee/githubAppManager.js";
import { RepoPermissionSyncer } from './ee/repoPermissionSyncer.js';
import { UserPermissionSyncer } from "./ee/userPermissionSyncer.js";
import { AccountPermissionSyncer } from "./ee/accountPermissionSyncer.js";
import { env } from "./env.js";
import { PromClient } from './promClient.js';
import { RepoIndexManager } from "./repoIndexManager.js";
Expand Down Expand Up @@ -52,7 +52,7 @@ if (hasEntitlement('github-app')) {

const connectionManager = new ConnectionManager(prisma, settings, redis, promClient);
const repoPermissionSyncer = new RepoPermissionSyncer(prisma, settings, redis);
const userPermissionSyncer = new UserPermissionSyncer(prisma, settings, redis);
const accountPermissionSyncer = new AccountPermissionSyncer(prisma, settings, redis);
const repoIndexManager = new RepoIndexManager(prisma, settings, redis, promClient);
const configManager = new ConfigManager(prisma, connectionManager, env.CONFIG_PATH);

Expand All @@ -65,7 +65,7 @@ if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && !hasEntitlement('per
}
else if (env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing')) {
repoPermissionSyncer.startScheduler();
userPermissionSyncer.startScheduler();
accountPermissionSyncer.startScheduler();
}

logger.info('Worker started.');
Expand All @@ -81,7 +81,7 @@ const cleanup = async (signal: string) => {
repoIndexManager.dispose(),
connectionManager.dispose(),
repoPermissionSyncer.dispose(),
userPermissionSyncer.dispose(),
accountPermissionSyncer.dispose(),
promClient.dispose(),
configManager.dispose(),
]),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
Warnings:

- You are about to drop the column `permissionSyncedAt` on the `User` table. All the data in the column will be lost.
- You are about to drop the `UserPermissionSyncJob` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `UserToRepoPermission` table. If the table is not empty, all the data it contains will be lost.

*/
-- CreateEnum
CREATE TYPE "AccountPermissionSyncJobStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED', 'FAILED');

-- DropForeignKey
ALTER TABLE "UserPermissionSyncJob" DROP CONSTRAINT "UserPermissionSyncJob_userId_fkey";

-- DropForeignKey
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_repoId_fkey";

-- DropForeignKey
ALTER TABLE "UserToRepoPermission" DROP CONSTRAINT "UserToRepoPermission_userId_fkey";

-- AlterTable
ALTER TABLE "Account" ADD COLUMN "permissionSyncedAt" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "User" DROP COLUMN "permissionSyncedAt";

-- DropTable
DROP TABLE "UserPermissionSyncJob";

-- DropTable
DROP TABLE "UserToRepoPermission";

-- DropEnum
DROP TYPE "UserPermissionSyncJobStatus";

-- CreateTable
CREATE TABLE "AccountPermissionSyncJob" (
"id" TEXT NOT NULL,
"status" "AccountPermissionSyncJobStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"completedAt" TIMESTAMP(3),
"errorMessage" TEXT,
"accountId" TEXT NOT NULL,

CONSTRAINT "AccountPermissionSyncJob_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "AccountToRepoPermission" (
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"repoId" INTEGER NOT NULL,
"accountId" TEXT NOT NULL,

CONSTRAINT "AccountToRepoPermission_pkey" PRIMARY KEY ("repoId","accountId")
);

-- AddForeignKey
ALTER TABLE "AccountPermissionSyncJob" ADD CONSTRAINT "AccountPermissionSyncJob_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "AccountToRepoPermission" ADD CONSTRAINT "AccountToRepoPermission_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;
29 changes: 16 additions & 13 deletions packages/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ model Repo {
connections RepoToConnection[]
imageUrl String?

permittedUsers UserToRepoPermission[]
permittedAccounts AccountToRepoPermission[]
permissionSyncJobs RepoPermissionSyncJob[]
permissionSyncedAt DateTime? /// When the permissions were last synced successfully.

Expand Down Expand Up @@ -349,7 +349,6 @@ model User {
accounts Account[]
orgs UserToOrg[]
accountRequest AccountRequest?
accessibleRepos UserToRepoPermission[]

/// List of pending invites that the user has created
invites Invite[]
Expand All @@ -361,40 +360,38 @@ model User {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

permissionSyncJobs UserPermissionSyncJob[]
permissionSyncedAt DateTime?
}

enum UserPermissionSyncJobStatus {
enum AccountPermissionSyncJobStatus {
PENDING
IN_PROGRESS
COMPLETED
FAILED
}

model UserPermissionSyncJob {
model AccountPermissionSyncJob {
id String @id @default(cuid())
status UserPermissionSyncJobStatus @default(PENDING)
status AccountPermissionSyncJobStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
completedAt DateTime?

errorMessage String?

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
accountId String
}

model UserToRepoPermission {
model AccountToRepoPermission {
createdAt DateTime @default(now())

repo Repo @relation(fields: [repoId], references: [id], onDelete: Cascade)
repoId Int

user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
accountId String

@@id([repoId, userId])
@@id([repoId, accountId])
}

// @see : https://authjs.dev/concepts/database-models#account
Expand All @@ -411,6 +408,12 @@ model Account {
scope String?
id_token String?
session_state String?

/// List of repos that this account has access to.
accessibleRepos AccountToRepoPermission[]

permissionSyncJobs AccountPermissionSyncJob[]
permissionSyncedAt DateTime?

createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Expand Down
18 changes: 11 additions & 7 deletions packages/web/src/prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,29 @@ if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
* Creates a prisma client extension that scopes queries to striclty information
* a given user should be able to access.
*/
export const userScopedPrismaClientExtension = (userId?: string) => {
export const userScopedPrismaClientExtension = (accountIds?: string[]) => {
return Prisma.defineExtension(
(prisma) => {
return prisma.$extends({
query: {
...(env.EXPERIMENT_EE_PERMISSION_SYNC_ENABLED === 'true' && hasEntitlement('permission-syncing') ? {
repo: {
async $allOperations({ args, query }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const argsWithWhere = args as any;
const argsWithWhere = args as Record<string, unknown> & {
where?: Prisma.RepoWhereInput;
}

argsWithWhere.where = {
...(argsWithWhere.where || {}),
OR: [
// Only include repos that are permitted to the user
...(userId ? [
...(accountIds ? [
{
permittedUsers: {
permittedAccounts: {
some: {
userId,
accountId: {
in: accountIds,
}
}
}
},
Expand All @@ -48,7 +52,7 @@ export const userScopedPrismaClientExtension = (userId?: string) => {
isPublic: true,
}
]
}
};

return query(args);
}
Expand Down
9 changes: 8 additions & 1 deletion packages/web/src/withAuthV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
},
}) : null;

const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(user?.id)) as PrismaClient;
const accountIds = user?.accounts.map(account => account.id);

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withOptionalAuthV2 > should call the callback with the auth context object if a valid session is present and the user is a member of the organization

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withOptionalAuthV2 src/withAuthV2.ts:44:25 ❯ src/withAuthV2.test.ts:483:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should return a service error if the user is not a member of the organization (guest role)

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:458:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should return a service error if the user is a guest of the organization

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:439:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role (api key)

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:386:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should call the callback with the auth context object if a valid session is present and the user is a member of the organization (api key)

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:351:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should call the callback with the auth context object if a valid session is present and the user is a member of the organization with OWNER role

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:316:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > withAuthV2 > should call the callback with the auth context object if a valid session is present and the user is a member of the organization

TypeError: Cannot read properties of undefined (reading 'map') ❯ getAuthContext src/withAuthV2.ts:91:30 ❯ Module.withAuthV2 src/withAuthV2.ts:28:25 ❯ src/withAuthV2.test.ts:286:24

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > getAuthContext > should return a auth context object if a valid session is present and the user is not a member of the organization. The role should be GUEST.

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.getAuthContext src/withAuthV2.ts:91:30 ❯ src/withAuthV2.test.ts:237:29

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > getAuthContext > should return a auth context object if a valid session is present and the user is a member of the organization with OWNER role

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.getAuthContext src/withAuthV2.ts:91:30 ❯ src/withAuthV2.test.ts:212:29

Check failure on line 91 in packages/web/src/withAuthV2.ts

View workflow job for this annotation

GitHub Actions / build

src/withAuthV2.test.ts > getAuthContext > should return a auth context object if a valid session is present and the user is a member of the organization

TypeError: Cannot read properties of undefined (reading 'map') ❯ Module.getAuthContext src/withAuthV2.ts:91:30 ❯ src/withAuthV2.test.ts:182:29
const prisma = __unsafePrisma.$extends(userScopedPrismaClientExtension(accountIds)) as PrismaClient;

return {
user: user ?? undefined,
Expand All @@ -106,6 +107,9 @@
const user = await __unsafePrisma.user.findUnique({
where: {
id: userId,
},
include: {
accounts: true,
}
});

Expand All @@ -125,6 +129,9 @@
where: {
id: apiKey.createdById,
},
include: {
accounts: true,
}
});

if (!user) {
Expand Down
Loading