mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 12:25:22 +00:00
Add data model for user <> repo permission link
This commit is contained in:
parent
83a8d306db
commit
b9a91c20fe
6 changed files with 143 additions and 51 deletions
|
|
@ -0,0 +1,14 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserToRepoPermission" (
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"repoId" INTEGER NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "UserToRepoPermission_pkey" PRIMARY KEY ("repoId","userId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserToRepoPermission" ADD CONSTRAINT "UserToRepoPermission_repoId_fkey" FOREIGN KEY ("repoId") REFERENCES "Repo"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserToRepoPermission" ADD CONSTRAINT "UserToRepoPermission_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
@ -56,6 +56,7 @@ model Repo {
|
||||||
connections RepoToConnection[]
|
connections RepoToConnection[]
|
||||||
imageUrl String?
|
imageUrl String?
|
||||||
repoIndexingStatus RepoIndexingStatus @default(NEW)
|
repoIndexingStatus RepoIndexingStatus @default(NEW)
|
||||||
|
permittedUsers UserToRepoPermission[]
|
||||||
|
|
||||||
// The id of the repo in the external service
|
// The id of the repo in the external service
|
||||||
external_id String
|
external_id String
|
||||||
|
|
@ -239,7 +240,6 @@ model ApiKey {
|
||||||
|
|
||||||
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade)
|
||||||
createdById String
|
createdById String
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model Audit {
|
model Audit {
|
||||||
|
|
@ -258,10 +258,8 @@ model Audit {
|
||||||
orgId Int
|
orgId Int
|
||||||
|
|
||||||
@@index([actorId, actorType, targetId, targetType, orgId])
|
@@index([actorId, actorType, targetId, targetType, orgId])
|
||||||
|
|
||||||
// Fast path for analytics queries – orgId is first because we assume most deployments are single tenant
|
// Fast path for analytics queries – orgId is first because we assume most deployments are single tenant
|
||||||
@@index([orgId, timestamp, action, actorId], map: "idx_audit_core_actions_full")
|
@@index([orgId, timestamp, action, actorId], map: "idx_audit_core_actions_full")
|
||||||
|
|
||||||
// Fast path for analytics queries for a specific user
|
// Fast path for analytics queries for a specific user
|
||||||
@@index([actorId, timestamp], map: "idx_audit_actor_time_full")
|
@@index([actorId, timestamp], map: "idx_audit_actor_time_full")
|
||||||
}
|
}
|
||||||
|
|
@ -277,6 +275,7 @@ model User {
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
orgs UserToOrg[]
|
orgs UserToOrg[]
|
||||||
accountRequest AccountRequest?
|
accountRequest AccountRequest?
|
||||||
|
accessibleRepos UserToRepoPermission[]
|
||||||
|
|
||||||
/// List of pending invites that the user has created
|
/// List of pending invites that the user has created
|
||||||
invites Invite[]
|
invites Invite[]
|
||||||
|
|
@ -289,6 +288,18 @@ model User {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserToRepoPermission {
|
||||||
|
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
|
||||||
|
|
||||||
|
@@id([repoId, userId])
|
||||||
|
}
|
||||||
|
|
||||||
// @see : https://authjs.dev/concepts/database-models#account
|
// @see : https://authjs.dev/concepts/database-models#account
|
||||||
model Account {
|
model Account {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
|
|
|
||||||
|
|
@ -639,7 +639,7 @@ export const getConnectionInfo = async (connectionId: number, domain: string) =>
|
||||||
})));
|
})));
|
||||||
|
|
||||||
export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
|
export const getRepos = async (filter: { status?: RepoIndexingStatus[], connectionId?: number } = {}) => sew(() =>
|
||||||
withOptionalAuthV2(async ({ org }) => {
|
withOptionalAuthV2(async ({ org, user }) => {
|
||||||
const repos = await prisma.repo.findMany({
|
const repos = await prisma.repo.findMany({
|
||||||
where: {
|
where: {
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
|
@ -653,6 +653,13 @@ export const getRepos = async (filter: { status?: RepoIndexingStatus[], connecti
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: user?.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
connections: {
|
connections: {
|
||||||
|
|
@ -722,6 +729,13 @@ export const getRepoInfoByName = async (repoName: string, domain: string) => sew
|
||||||
where: {
|
where: {
|
||||||
name: repoName,
|
name: repoName,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -889,6 +903,13 @@ export const experimental_addGithubRepositoryByUrl = async (repositoryUrl: strin
|
||||||
external_id: githubRepo.id.toString(),
|
external_id: githubRepo.id.toString(),
|
||||||
external_codeHostType: 'github',
|
external_codeHostType: 'github',
|
||||||
external_codeHostUrl: 'https://github.com',
|
external_codeHostUrl: 'https://github.com',
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1039,6 +1060,13 @@ export const flagReposForIndex = async (repoIds: number[], domain: string) => se
|
||||||
where: {
|
where: {
|
||||||
id: { in: repoIds },
|
id: { in: repoIds },
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
repoIndexingStatus: RepoIndexingStatus.NEW,
|
repoIndexingStatus: RepoIndexingStatus.NEW,
|
||||||
|
|
@ -2021,6 +2049,13 @@ export const getRepoImage = async (repoId: number, domain: string): Promise<Arra
|
||||||
where: {
|
where: {
|
||||||
id: repoId,
|
id: repoId,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
connections: {
|
connections: {
|
||||||
|
|
@ -2028,7 +2063,7 @@ export const getRepoImage = async (repoId: number, domain: string): Promise<Arra
|
||||||
connection: true,
|
connection: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!repo || !repo.imageUrl) {
|
if (!repo || !repo.imageUrl) {
|
||||||
|
|
|
||||||
|
|
@ -136,6 +136,8 @@ export const env = createEnv({
|
||||||
EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED: booleanSchema.default('false'),
|
EXPERIMENT_SELF_SERVE_REPO_INDEXING_ENABLED: booleanSchema.default('false'),
|
||||||
// @NOTE: Take care to update actions.ts when changing the name of this.
|
// @NOTE: Take care to update actions.ts when changing the name of this.
|
||||||
EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN: z.string().optional(),
|
EXPERIMENT_SELF_SERVE_REPO_INDEXING_GITHUB_TOKEN: z.string().optional(),
|
||||||
|
|
||||||
|
EXPERIMENT_PERMISSION_SYNC_ENABLED: booleanSchema.default('false'),
|
||||||
},
|
},
|
||||||
// @NOTE: Please make sure of the following:
|
// @NOTE: Please make sure of the following:
|
||||||
// - Make sure you destructure all client variables in
|
// - Make sure you destructure all client variables in
|
||||||
|
|
|
||||||
|
|
@ -26,13 +26,20 @@ export type FileTreeNode = FileTreeItem & {
|
||||||
* at a given revision.
|
* at a given revision.
|
||||||
*/
|
*/
|
||||||
export const getTree = async (params: { repoName: string, revisionName: string }, domain: string) => sew(() =>
|
export const getTree = async (params: { repoName: string, revisionName: string }, domain: string) => sew(() =>
|
||||||
withAuth((session) =>
|
withAuth((userId) =>
|
||||||
withOrgMembership(session, domain, async ({ org }) => {
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
const { repoName, revisionName } = params;
|
const { repoName, revisionName } = params;
|
||||||
const repo = await prisma.repo.findFirst({
|
const repo = await prisma.repo.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: repoName,
|
name: repoName,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -85,13 +92,20 @@ export const getTree = async (params: { repoName: string, revisionName: string }
|
||||||
* at a given revision.
|
* at a given revision.
|
||||||
*/
|
*/
|
||||||
export const getFolderContents = async (params: { repoName: string, revisionName: string, path: string }, domain: string) => sew(() =>
|
export const getFolderContents = async (params: { repoName: string, revisionName: string, path: string }, domain: string) => sew(() =>
|
||||||
withAuth((session) =>
|
withAuth((userId) =>
|
||||||
withOrgMembership(session, domain, async ({ org }) => {
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
const { repoName, revisionName, path } = params;
|
const { repoName, revisionName, path } = params;
|
||||||
const repo = await prisma.repo.findFirst({
|
const repo = await prisma.repo.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: repoName,
|
name: repoName,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -158,14 +172,21 @@ export const getFolderContents = async (params: { repoName: string, revisionName
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getFiles = async (params: { repoName: string, revisionName: string }, domain: string) => sew(() =>
|
export const getFiles = async (params: { repoName: string, revisionName: string }, domain: string) => sew(() =>
|
||||||
withAuth((session) =>
|
withAuth((userId) =>
|
||||||
withOrgMembership(session, domain, async ({ org }) => {
|
withOrgMembership(userId, domain, async ({ org }) => {
|
||||||
const { repoName, revisionName } = params;
|
const { repoName, revisionName } = params;
|
||||||
|
|
||||||
const repo = await prisma.repo.findFirst({
|
const repo = await prisma.repo.findFirst({
|
||||||
where: {
|
where: {
|
||||||
name: repoName,
|
name: repoName,
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId: userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import { StatusCodes } from "http-status-codes";
|
||||||
import { zoektSearchResponseSchema } from "./zoektSchema";
|
import { zoektSearchResponseSchema } from "./zoektSchema";
|
||||||
import { SearchRequest, SearchResponse, SourceRange } from "./types";
|
import { SearchRequest, SearchResponse, SourceRange } from "./types";
|
||||||
import { OrgRole, Repo } from "@sourcebot/db";
|
import { OrgRole, Repo } from "@sourcebot/db";
|
||||||
import * as Sentry from "@sentry/nextjs";
|
|
||||||
import { sew, withAuth, withOrgMembership } from "@/actions";
|
import { sew, withAuth, withOrgMembership } from "@/actions";
|
||||||
import { base64Decode } from "@sourcebot/shared";
|
import { base64Decode } from "@sourcebot/shared";
|
||||||
|
|
||||||
|
|
@ -204,6 +203,13 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
|
||||||
in: Array.from(repoIdentifiers).filter((id) => typeof id === "number"),
|
in: Array.from(repoIdentifiers).filter((id) => typeof id === "number"),
|
||||||
},
|
},
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
}
|
}
|
||||||
})).forEach(repo => repos.set(repo.id, repo));
|
})).forEach(repo => repos.set(repo.id, repo));
|
||||||
|
|
||||||
|
|
@ -213,6 +219,13 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
|
||||||
in: Array.from(repoIdentifiers).filter((id) => typeof id === "string"),
|
in: Array.from(repoIdentifiers).filter((id) => typeof id === "string"),
|
||||||
},
|
},
|
||||||
orgId: org.id,
|
orgId: org.id,
|
||||||
|
...(env.EXPERIMENT_PERMISSION_SYNC_ENABLED === 'true' ? {
|
||||||
|
permittedUsers: {
|
||||||
|
some: {
|
||||||
|
userId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} : {})
|
||||||
}
|
}
|
||||||
})).forEach(repo => repos.set(repo.name, repo));
|
})).forEach(repo => repos.set(repo.name, repo));
|
||||||
|
|
||||||
|
|
@ -234,12 +247,8 @@ export const search = async ({ query, matches, contextLines, whole }: SearchRequ
|
||||||
const identifier = file.RepositoryID ?? file.Repository;
|
const identifier = file.RepositoryID ?? file.Repository;
|
||||||
const repo = repos.get(identifier);
|
const repo = repos.get(identifier);
|
||||||
|
|
||||||
// This should never happen... but if it does, we skip the file.
|
// This can happen if the user doesn't have access to the repository.
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
Sentry.captureMessage(
|
|
||||||
`Repository not found for identifier: ${identifier}; skipping file "${file.FileName}"`,
|
|
||||||
'warning'
|
|
||||||
);
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue