Skip to content
Merged
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,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
6 changes: 3 additions & 3 deletions packages/web/src/__mocks__/prisma.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SINGLE_TENANT_ORG_DOMAIN, SINGLE_TENANT_ORG_ID, SINGLE_TENANT_ORG_NAME } from '@/lib/constants';
import { ApiKey, Org, PrismaClient, User } from '@prisma/client';
import { Account, ApiKey, Org, PrismaClient, User } from '@prisma/client';
import { beforeEach, vi } from 'vitest';
import { mockDeep, mockReset } from 'vitest-mock-extended';

Expand Down Expand Up @@ -35,7 +35,7 @@ export const MOCK_API_KEY: ApiKey = {
createdById: '1',
}

export const MOCK_USER: User = {
export const MOCK_USER_WITH_ACCOUNTS: User & { accounts: Account[] } = {
id: '1',
name: 'Test User',
email: '[email protected]',
Expand All @@ -44,7 +44,7 @@ export const MOCK_USER: User = {
hashedPassword: null,
emailVerified: null,
image: null,
permissionSyncedAt: null
accounts: [],
}

export const userScopedPrismaClientExtension = vi.fn();
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
Loading
Loading