Skip to content

Commit 75d4189

Browse files
authored
enforce tenancy on search and repo listing endpoints (#181)
* enforce tenancy on search and repo listing * remove orgId from request schemas
1 parent a88f9e6 commit 75d4189

13 files changed

Lines changed: 207 additions & 211 deletions

File tree

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
"build": "yarn workspaces run build",
88
"test": "yarn workspaces run test",
99
"dev": "cross-env SOURCEBOT_TENANT_MODE=single npm-run-all --print-label dev:start",
10-
"dev:mt": "cross-env SOURCEBOT_TENANT_MODE=multi npm-run-all --print-label dev:start",
10+
"dev:mt": "cross-env SOURCEBOT_TENANT_MODE=multi npm-run-all --print-label dev:start:mt",
1111
"dev:start": "yarn workspace @sourcebot/db prisma:migrate:dev && cross-env npm-run-all --print-label --parallel dev:zoekt dev:backend dev:web",
12+
"dev:start:mt": "yarn workspace @sourcebot/db prisma:migrate:dev && cross-env npm-run-all --print-label --parallel dev:zoekt:mt dev:backend dev:web",
1213
"dev:zoekt": "export PATH=\"$PWD/bin:$PATH\" && export SRC_TENANT_ENFORCEMENT_MODE=none && zoekt-webserver -index .sourcebot/index -rpc",
14+
"dev:zoekt:mt": "export PATH=\"$PWD/bin:$PATH\" && export SRC_TENANT_ENFORCEMENT_MODE=strict && zoekt-webserver -index .sourcebot/index -rpc",
1315
"dev:backend": "yarn workspace @sourcebot/backend dev:watch",
1416
"dev:web": "yarn workspace @sourcebot/web dev"
1517
},

packages/web/src/actions.ts

Lines changed: 16 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
'use server';
22

33
import Ajv from "ajv";
4-
import { getUser } from "./data/user";
5-
import { auth } from "./auth";
6-
import { notAuthenticated, notFound, ServiceError, unexpectedError } from "./lib/serviceError";
4+
import { auth, getCurrentUserOrg } from "./auth";
5+
import { notAuthenticated, notFound, ServiceError, unexpectedError } from "@/lib/serviceError";
76
import { prisma } from "@/prisma";
87
import { StatusCodes } from "http-status-codes";
9-
import { ErrorCode } from "./lib/errorCodes";
8+
import { ErrorCode } from "@/lib/errorCodes";
9+
import { isServiceError } from "@/lib/utils";
1010
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
1111
import { encrypt } from "@sourcebot/crypto"
1212

@@ -15,31 +15,9 @@ const ajv = new Ajv({
1515
});
1616

1717
export const createSecret = async (key: string, value: string): Promise<{ success: boolean } | ServiceError> => {
18-
const session = await auth();
19-
if (!session) {
20-
return notAuthenticated();
21-
}
22-
23-
const user = await getUser(session.user.id);
24-
if (!user) {
25-
return unexpectedError("User not found");
26-
}
27-
const orgId = user.activeOrgId;
28-
if (!orgId) {
29-
return unexpectedError("User has no active org");
30-
}
31-
32-
// @todo: refactor this into a shared function
33-
const membership = await prisma.userToOrg.findUnique({
34-
where: {
35-
orgId_userId: {
36-
userId: session.user.id,
37-
orgId,
38-
}
39-
},
40-
});
41-
if (!membership) {
42-
return notFound();
18+
const orgId = await getCurrentUserOrg();
19+
if (isServiceError(orgId)) {
20+
return orgId;
4321
}
4422

4523
try {
@@ -62,30 +40,9 @@ export const createSecret = async (key: string, value: string): Promise<{ succes
6240
}
6341

6442
export const getSecrets = async (): Promise<{ createdAt: Date; key: string; }[] | ServiceError> => {
65-
const session = await auth();
66-
if (!session) {
67-
return notAuthenticated();
68-
}
69-
70-
const user = await getUser(session.user.id);
71-
if (!user) {
72-
return unexpectedError("User not found");
73-
}
74-
const orgId = user.activeOrgId;
75-
if (!orgId) {
76-
return unexpectedError("User has no active org");
77-
}
78-
79-
const membership = await prisma.userToOrg.findUnique({
80-
where: {
81-
orgId_userId: {
82-
userId: session.user.id,
83-
orgId,
84-
}
85-
},
86-
});
87-
if (!membership) {
88-
return notFound();
43+
const orgId = await getCurrentUserOrg();
44+
if (isServiceError(orgId)) {
45+
return orgId;
8946
}
9047

9148
const secrets = await prisma.secret.findMany({
@@ -105,30 +62,9 @@ export const getSecrets = async (): Promise<{ createdAt: Date; key: string; }[]
10562
}
10663

10764
export const deleteSecret = async (key: string): Promise<{ success: boolean } | ServiceError> => {
108-
const session = await auth();
109-
if (!session) {
110-
return notAuthenticated();
111-
}
112-
113-
const user = await getUser(session.user.id);
114-
if (!user) {
115-
return unexpectedError("User not found");
116-
}
117-
const orgId = user.activeOrgId;
118-
if (!orgId) {
119-
return unexpectedError("User has no active org");
120-
}
121-
122-
const membership = await prisma.userToOrg.findUnique({
123-
where: {
124-
orgId_userId: {
125-
userId: session.user.id,
126-
orgId,
127-
}
128-
},
129-
});
130-
if (!membership) {
131-
return notFound();
65+
const orgId = await getCurrentUserOrg();
66+
if (isServiceError(orgId)) {
67+
return orgId;
13268
}
13369

13470
await prisma.secret.delete({
@@ -206,31 +142,9 @@ export const switchActiveOrg = async (orgId: number): Promise<{ id: number } | S
206142
}
207143

208144
export const createConnection = async (config: string): Promise<{ id: number } | ServiceError> => {
209-
const session = await auth();
210-
if (!session) {
211-
return notAuthenticated();
212-
}
213-
214-
const user = await getUser(session.user.id);
215-
if (!user) {
216-
return unexpectedError("User not found");
217-
}
218-
const orgId = user.activeOrgId;
219-
if (!orgId) {
220-
return unexpectedError("User has no active org");
221-
}
222-
223-
// @todo: refactor this into a shared function
224-
const membership = await prisma.userToOrg.findUnique({
225-
where: {
226-
orgId_userId: {
227-
userId: session.user.id,
228-
orgId,
229-
}
230-
},
231-
});
232-
if (!membership) {
233-
return notFound();
145+
const orgId = await getCurrentUserOrg();
146+
if (isServiceError(orgId)) {
147+
return orgId;
234148
}
235149

236150
let parsedConfig;
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
'use server';
22

33
import { listRepositories } from "@/lib/server/searchService";
4+
import { getCurrentUserOrg } from "../../../../auth";
5+
import { isServiceError } from "@/lib/utils";
46

57
export const GET = async () => {
6-
const response = await listRepositories();
8+
const orgId = await getCurrentUserOrg();
9+
if (isServiceError(orgId)) {
10+
return orgId;
11+
}
12+
13+
const response = await listRepositories(orgId);
714
return Response.json(response);
815
}

packages/web/src/app/api/(server)/search/route.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,25 @@ import { searchRequestSchema } from "@/lib/schemas";
55
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
66
import { isServiceError } from "@/lib/utils";
77
import { NextRequest } from "next/server";
8+
import { getCurrentUserOrg } from "../../../../auth";
89

910
export const POST = async (request: NextRequest) => {
10-
const body = await request.json();
11-
const tenantId = request.headers.get("X-Tenant-ID");
12-
13-
console.log(`Search request received. Tenant ID: ${tenantId}`);
11+
const orgId = await getCurrentUserOrg();
12+
if (isServiceError(orgId)) {
13+
return orgId;
14+
}
1415

15-
const parsed = await searchRequestSchema.safeParseAsync({
16-
...body,
17-
...(tenantId ? {
18-
tenantId: parseInt(tenantId)
19-
} : {}),
20-
});
16+
console.log(`Searching for org ${orgId}`);
17+
const body = await request.json();
18+
const parsed = await searchRequestSchema.safeParseAsync(body);
2119
if (!parsed.success) {
2220
return serviceErrorResponse(
2321
schemaValidationError(parsed.error)
2422
);
2523
}
2624

2725

28-
const response = await search(parsed.data);
26+
const response = await search(parsed.data, orgId);
2927
if (isServiceError(response)) {
3028
return serviceErrorResponse(response);
3129
}

packages/web/src/app/api/(server)/source/route.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ import { getFileSource } from "@/lib/server/searchService";
55
import { schemaValidationError, serviceErrorResponse } from "@/lib/serviceError";
66
import { isServiceError } from "@/lib/utils";
77
import { NextRequest } from "next/server";
8+
import { getCurrentUserOrg } from "@/auth";
89

910
export const POST = async (request: NextRequest) => {
11+
const orgId = await getCurrentUserOrg();
12+
if (isServiceError(orgId)) {
13+
return orgId;
14+
}
15+
1016
const body = await request.json();
1117
const parsed = await fileSourceRequestSchema.safeParseAsync(body);
1218
if (!parsed.success) {
@@ -15,7 +21,7 @@ export const POST = async (request: NextRequest) => {
1521
);
1622
}
1723

18-
const response = await getFileSource(parsed.data);
24+
const response = await getFileSource(parsed.data, orgId);
1925
if (isServiceError(response)) {
2026
return serviceErrorResponse(response);
2127
}

packages/web/src/app/browse/[...path]/page.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CodePreview } from "./codePreview";
77
import { PageNotFound } from "@/app/components/pageNotFound";
88
import { ErrorCode } from "@/lib/errorCodes";
99
import { LuFileX2, LuBookX } from "react-icons/lu";
10+
import { getCurrentUserOrg } from "@/auth";
1011

1112
interface BrowsePageProps {
1213
params: {
@@ -44,9 +45,18 @@ export default async function BrowsePage({
4445
}
4546
})();
4647

48+
const orgId = await getCurrentUserOrg();
49+
if (isServiceError(orgId)) {
50+
return (
51+
<>
52+
Error: {orgId.message}
53+
</>
54+
)
55+
}
56+
4757
// @todo (bkellam) : We should probably have a endpoint to fetch repository metadata
4858
// given it's name or id.
49-
const reposResponse = await listRepositories();
59+
const reposResponse = await listRepositories(orgId);
5060
if (isServiceError(reposResponse)) {
5161
// @todo : proper error handling
5262
return (
@@ -98,6 +108,7 @@ export default async function BrowsePage({
98108
path={path}
99109
repoName={repoName}
100110
revisionName={revisionName ?? 'HEAD'}
111+
orgId={orgId}
101112
/>
102113
)}
103114
</div>
@@ -108,19 +119,21 @@ interface CodePreviewWrapper {
108119
path: string,
109120
repoName: string,
110121
revisionName: string,
122+
orgId: number,
111123
}
112124

113125
const CodePreviewWrapper = async ({
114126
path,
115127
repoName,
116128
revisionName,
129+
orgId,
117130
}: CodePreviewWrapper) => {
118131
// @todo: this will depend on `pathType`.
119132
const fileSourceResponse = await getFileSource({
120133
fileName: path,
121134
repository: repoName,
122135
branch: revisionName,
123-
});
136+
}, orgId);
124137

125138
if (isServiceError(fileSourceResponse)) {
126139
if (fileSourceResponse.errorCode === ErrorCode.FILE_NOT_FOUND) {

0 commit comments

Comments
 (0)