mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 09:41:04 +01:00
fix: merge tool access existence oracle
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
3 files changed
+195
-39
No files matched your search
@@ -7,11 +7,12 @@ import { fileURLToPath } from "node:url";
|
||||
* Static guard against the cross-tenant existence oracle.
|
||||
*
|
||||
* Route handlers that look a resource up by id and then call
|
||||
* `assertCompanyAccess(req, resource.companyId)` leak resource existence
|
||||
* across tenants: missing ids return 404 while cross-tenant ids return 403,
|
||||
* letting any authenticated user enumerate other tenants' ids. The required
|
||||
* pattern (documented on `hasCompanyAccess` in routes/authz.ts) folds the
|
||||
* access check into the existence check:
|
||||
* `assertCompanyAccess(req, resource.companyId)`, or a local wrapper helper
|
||||
* that internally calls it, leak resource existence across tenants: missing ids
|
||||
* return 404 while cross-tenant ids return 403, letting any authenticated user
|
||||
* enumerate other tenants' ids. The required pattern (documented on
|
||||
* `hasCompanyAccess` in routes/authz.ts) folds the access check into the
|
||||
* existence check:
|
||||
*
|
||||
* const issue = await svc.getById(id);
|
||||
* if (!issue || !hasCompanyAccess(req, issue.companyId)) {
|
||||
@@ -20,9 +21,9 @@ import { fileURLToPath } from "node:url";
|
||||
* }
|
||||
* assertCompanyAccess(req, issue.companyId); // write paths only
|
||||
*
|
||||
* This test scans every route file and fails when it finds an
|
||||
* `assertCompanyAccess(req, <resource>.companyId)` call that is not preceded
|
||||
* by a `hasCompanyAccess(req, <resource>.companyId)` gate. Sites where the
|
||||
* This test scans every route file and fails when it finds a company access
|
||||
* assertion on `req, <resource>.companyId` that is not preceded by a
|
||||
* `hasCompanyAccess(req, <resource>.companyId)`-style gate. Sites where the
|
||||
* companyId comes from request input (params/body) rather than a looked-up
|
||||
* resource carry no oracle and belong on the allowlist below.
|
||||
*/
|
||||
@@ -30,8 +31,8 @@ import { fileURLToPath } from "node:url";
|
||||
const ROUTES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "routes");
|
||||
|
||||
// "<file>:<variable>" call sites where the companyId is request input, not a
|
||||
// discovered resource — no existence oracle to close. Add new entries only
|
||||
// when the value cannot reveal whether a cross-tenant resource exists.
|
||||
// discovered resource. Add new entries only when the value cannot reveal
|
||||
// whether a cross-tenant resource exists.
|
||||
const REQUEST_INPUT_ALLOWLIST = new Set([
|
||||
"companies.ts:target", // import target chosen by the caller (req.body.target)
|
||||
"plugins.ts:runContext", // run context supplied in the request body
|
||||
@@ -39,35 +40,70 @@ const REQUEST_INPUT_ALLOWLIST = new Set([
|
||||
|
||||
const GATE_LOOKBACK_LINES = 12;
|
||||
|
||||
const COMPANY_ACCESS_ASSERTION_HELPERS = [
|
||||
"assertCompanyAccess",
|
||||
"assertBoardToolPermission",
|
||||
"assertBoardAnyToolPermission",
|
||||
"assertToolAppMutationAccess",
|
||||
"assertToolsAdmin",
|
||||
"assertToolsRuntimeManage",
|
||||
] as const;
|
||||
|
||||
const LOOKED_UP_RESOURCE_GATE_HELPERS = [
|
||||
"hasCompanyAccess",
|
||||
"assertLookedUpToolResourceAccess",
|
||||
] as const;
|
||||
|
||||
function helperAlternation(helpers: readonly string[]) {
|
||||
return helpers.join("|");
|
||||
}
|
||||
|
||||
const companyAccessAssertionPattern = new RegExp(
|
||||
`\\b(${helperAlternation(COMPANY_ACCESS_ASSERTION_HELPERS)})\\(req,\\s*(\\w+)\\.companyId\\b`,
|
||||
);
|
||||
|
||||
function gatePatternFor(variable: string) {
|
||||
return new RegExp(
|
||||
`\\b(${helperAlternation(LOOKED_UP_RESOURCE_GATE_HELPERS)})\\(req,\\s*${variable}\\.companyId\\b`,
|
||||
);
|
||||
}
|
||||
|
||||
function assertionPatternFor(variable: string) {
|
||||
return new RegExp(
|
||||
`\\b(${helperAlternation(COMPANY_ACCESS_ASSERTION_HELPERS)})\\(req,\\s*${variable}\\.companyId\\b`,
|
||||
);
|
||||
}
|
||||
|
||||
function findUngatedSites() {
|
||||
const ungated: string[] = [];
|
||||
for (const file of readdirSync(ROUTES_DIR).filter((name) => name.endsWith(".ts"))) {
|
||||
if (file === "authz.ts") continue;
|
||||
const lines = readFileSync(join(ROUTES_DIR, file), "utf8").split("\n");
|
||||
lines.forEach((line, index) => {
|
||||
const match = line.match(/assertCompanyAccess\(req,\s*(\w+)\.companyId\)/);
|
||||
const match = line.match(companyAccessAssertionPattern);
|
||||
if (!match) return;
|
||||
const variable = match[1];
|
||||
const [, helper, variable] = match;
|
||||
if (!helper || !variable) return;
|
||||
const lookback = lines
|
||||
.slice(Math.max(0, index - GATE_LOOKBACK_LINES), index)
|
||||
.join("\n");
|
||||
if (new RegExp(`hasCompanyAccess\\(req,\\s*${variable}\\.companyId\\)`).test(lookback)) return;
|
||||
if (gatePatternFor(variable).test(lookback)) return;
|
||||
if (REQUEST_INPUT_ALLOWLIST.has(`${file}:${variable}`)) return;
|
||||
ungated.push(`${file}:${index + 1} (${variable}.companyId)`);
|
||||
ungated.push(`${file}:${index + 1} (${helper} on ${variable}.companyId)`);
|
||||
});
|
||||
}
|
||||
return ungated;
|
||||
}
|
||||
|
||||
describe("cross-tenant existence oracle guard", () => {
|
||||
it("requires a hasCompanyAccess gate before assertCompanyAccess on looked-up resources", () => {
|
||||
it("requires a 404 gate before company access wrappers on looked-up resources", () => {
|
||||
const ungated = findUngatedSites();
|
||||
expect(
|
||||
ungated,
|
||||
"assertCompanyAccess on a looked-up resource without a hasCompanyAccess 404 gate "
|
||||
+ "reintroduces the cross-tenant existence oracle (403 vs 404). "
|
||||
"Company access wrappers on a looked-up resource without a hasCompanyAccess 404 gate "
|
||||
+ "reintroduce the cross-tenant existence oracle (403 vs 404). "
|
||||
+ "Apply the two-step pattern documented on hasCompanyAccess in routes/authz.ts, "
|
||||
+ "or — only if the companyId is request input — add the site to REQUEST_INPUT_ALLOWLIST. "
|
||||
+ "or, only if the companyId is request input, add the site to REQUEST_INPUT_ALLOWLIST. "
|
||||
+ `Offending sites: ${ungated.join(", ")}`,
|
||||
).toEqual([]);
|
||||
});
|
||||
@@ -77,7 +113,7 @@ describe("cross-tenant existence oracle guard", () => {
|
||||
for (const entry of REQUEST_INPUT_ALLOWLIST) {
|
||||
const [file, variable] = entry.split(":");
|
||||
const source = readFileSync(join(ROUTES_DIR, file!), "utf8");
|
||||
if (!new RegExp(`assertCompanyAccess\\(req,\\s*${variable}\\.companyId\\)`).test(source)) {
|
||||
if (!variable || !assertionPatternFor(variable).test(source)) {
|
||||
stale.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,6 +1146,102 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns identical 404s for cross-tenant and missing wrapper-gated connection lookups", async () => {
|
||||
const ownerCompany = await createCompany(db);
|
||||
const outsiderCompany = await createCompany(db);
|
||||
const userId = `tool-outsider-${randomUUID()}`;
|
||||
await grantBoardUser(db, outsiderCompany.id, userId, ["tools:use", "tools:manage_connections"]);
|
||||
const actor = boardSessionActor(outsiderCompany.id, "operator", userId);
|
||||
const agent = await createAgent(db, ownerCompany.id);
|
||||
const { connection } = await createRemoteToolFixture(db, ownerCompany.id);
|
||||
const app = createRouteApp(db, actor, createToolGatewayService(db, { toolActionSigningSecret: "test-secret" }));
|
||||
const missingConnectionId = randomUUID();
|
||||
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
request: (connectionId: string) => request.Test;
|
||||
}> = [
|
||||
{
|
||||
name: "GET /tool-connections/:connectionId/test-agents",
|
||||
request: (connectionId) => request(app).get(`/api/tool-connections/${connectionId}/test-agents`),
|
||||
},
|
||||
{
|
||||
name: "POST /tool-connections/:connectionId/test-calls",
|
||||
request: (connectionId) => request(app)
|
||||
.post(`/api/tool-connections/${connectionId}/test-calls`)
|
||||
.send({ agentId: agent.id, toolName: "send_email", parameters: { to: "a@example.com" } }),
|
||||
},
|
||||
{
|
||||
name: "PUT /tool-connections/:connectionId/installs",
|
||||
request: (connectionId) => request(app)
|
||||
.put(`/api/tool-connections/${connectionId}/installs`)
|
||||
.send({ installs: [] }),
|
||||
},
|
||||
{
|
||||
name: "POST /tools/oauth/:connectionId/start",
|
||||
request: (connectionId) => request(app).post(`/api/tools/oauth/${connectionId}/start`),
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const crossTenant = await testCase.request(connection.id);
|
||||
const missing = await testCase.request(missingConnectionId);
|
||||
expect(crossTenant.status, testCase.name).toBe(404);
|
||||
expect(crossTenant.body, testCase.name).toEqual(missing.body);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns identical 404s for cross-tenant and missing wrapper-gated application lookups", async () => {
|
||||
const ownerCompany = await createCompany(db);
|
||||
const outsiderCompany = await createCompany(db);
|
||||
const userId = `app-outsider-${randomUUID()}`;
|
||||
await grantBoardUser(db, outsiderCompany.id, userId, ["tools:admin"]);
|
||||
const actor = boardSessionActor(outsiderCompany.id, "operator", userId);
|
||||
const { application } = await createRemoteToolFixture(db, ownerCompany.id);
|
||||
const app = createRouteApp(db, actor);
|
||||
const missingApplicationId = randomUUID();
|
||||
|
||||
const cases: Array<{
|
||||
name: string;
|
||||
request: (applicationId: string) => request.Test;
|
||||
}> = [
|
||||
{
|
||||
name: "PATCH /tool-applications/:applicationId",
|
||||
request: (applicationId) => request(app)
|
||||
.patch(`/api/tool-applications/${applicationId}`)
|
||||
.send({ name: `Renamed ${randomUUID()}` }),
|
||||
},
|
||||
{
|
||||
name: "DELETE /tool-applications/:applicationId",
|
||||
request: (applicationId) => request(app).delete(`/api/tool-applications/${applicationId}`),
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const crossTenant = await testCase.request(application.id);
|
||||
const missing = await testCase.request(missingApplicationId);
|
||||
expect(crossTenant.status, testCase.name).toBe(404);
|
||||
expect(crossTenant.body, testCase.name).toEqual(missing.body);
|
||||
}
|
||||
});
|
||||
|
||||
it("still returns 403 for same-company wrapper permission denials", async () => {
|
||||
const company = await createCompany(db);
|
||||
const userId = `tool-denied-${randomUUID()}`;
|
||||
await grantBoardUser(db, company.id, userId, []);
|
||||
const { connection } = await createRemoteToolFixture(db, company.id);
|
||||
const app = createRouteApp(
|
||||
db,
|
||||
boardSessionActor(company.id, "operator", userId),
|
||||
createToolGatewayService(db, { toolActionSigningSecret: "test-secret" }),
|
||||
);
|
||||
|
||||
const res = await request(app).get(`/api/tool-connections/${connection.id}/test-agents`);
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
expect(res.body.error).toContain("Missing one of permissions");
|
||||
});
|
||||
|
||||
it("surfaces a last-changed audit hint attributed to the agent that authored the governing policy", async () => {
|
||||
const company = await createCompany(db);
|
||||
const userId = `tool-tester-${randomUUID()}`;
|
||||
@@ -2047,7 +2143,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
.expect(403);
|
||||
});
|
||||
|
||||
it("returns 404 for cross-company profile reads, 403 for mutations, and 404 for missing profiles", async () => {
|
||||
it("returns 404 for cross-company and missing profile routes", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const profile = await toolAccessService(db).createProfile(otherCompany.id, {
|
||||
@@ -2076,15 +2172,15 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/duplicate`)
|
||||
.send({ name: "Forbidden copy", includeAssignments: false })
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
await request(app)
|
||||
.delete(`/api/tool-profiles/${profile.id}`)
|
||||
.send({ force: false })
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/new-tools/review`)
|
||||
.send({ decisions: [{ catalogEntryId: randomUUID(), decision: "keep_blocked" }] })
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
|
||||
await request(createRouteApp(db)).get(`/api/tool-profiles/${randomUUID()}/new-tools`).expect(404);
|
||||
await request(createRouteApp(db))
|
||||
@@ -4726,7 +4822,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 403 for cross-company application updates and 404 for missing applications", async () => {
|
||||
it("returns 404 for cross-company and missing application updates", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const application = await toolAccessService(db).createApplication(otherCompany.id, {
|
||||
@@ -4750,14 +4846,15 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
source: "session",
|
||||
});
|
||||
|
||||
const forbiddenRes = await request(app)
|
||||
const crossTenantRes = await request(app)
|
||||
.patch(`/api/tool-applications/${application.id}`)
|
||||
.send({ name: "Forbidden edit" });
|
||||
const missingRes = await request(createRouteApp(db))
|
||||
.patch(`/api/tool-applications/${randomUUID()}`)
|
||||
.send({ name: "Missing edit" });
|
||||
|
||||
expect(forbiddenRes.status).toBe(403);
|
||||
expect(crossTenantRes.status).toBe(404);
|
||||
expect(crossTenantRes.body).toEqual(missingRes.body);
|
||||
expect(missingRes.status).toBe(404);
|
||||
});
|
||||
|
||||
@@ -5082,7 +5179,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
expect(remainingConnection).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("returns 403 for cross-company application deletes and 404 for missing applications", async () => {
|
||||
it("returns 404 for cross-company and missing application deletes", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const application = await toolAccessService(db).createApplication(otherCompany.id, {
|
||||
@@ -5106,10 +5203,11 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
source: "session",
|
||||
});
|
||||
|
||||
const forbiddenRes = await request(app).delete(`/api/tool-applications/${application.id}`);
|
||||
const crossTenantRes = await request(app).delete(`/api/tool-applications/${application.id}`);
|
||||
const missingRes = await request(createRouteApp(db)).delete(`/api/tool-applications/${randomUUID()}`);
|
||||
|
||||
expect(forbiddenRes.status).toBe(403);
|
||||
expect(crossTenantRes.status).toBe(404);
|
||||
expect(crossTenantRes.body).toEqual(missingRes.body);
|
||||
expect(missingRes.status).toBe(404);
|
||||
const stillThere = await db
|
||||
.select()
|
||||
|
||||
@@ -212,6 +212,13 @@ export function toolAccessRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
function assertLookedUpToolResourceAccess(req: Request, companyId: string, notFoundMessage: string) {
|
||||
if (!hasCompanyAccess(req, companyId)) {
|
||||
throw notFound(notFoundMessage);
|
||||
}
|
||||
assertCompanyAccess(req, companyId);
|
||||
}
|
||||
|
||||
router.get("/companies/:companyId/tools/gallery", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const companyId = req.params.companyId as string;
|
||||
@@ -267,6 +274,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tools/oauth/:connectionId/start", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.startOAuth(existing.companyId, existing.id, {
|
||||
redirectUri: oauthRedirectUri(),
|
||||
@@ -285,6 +293,7 @@ export function toolAccessRoutes(
|
||||
if (!pendingState) {
|
||||
throw badRequest("Invalid or expired OAuth state");
|
||||
}
|
||||
assertLookedUpToolResourceAccess(req, pendingState.companyId, "Invalid or expired OAuth state");
|
||||
assertToolAppMutationAccess(req, pendingState.companyId);
|
||||
const result = await svc.completeOAuthCallback({
|
||||
state,
|
||||
@@ -447,6 +456,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.patch("/tool-applications/:applicationId", validate(updateToolApplicationSchema), async (req, res) => {
|
||||
const existing = await svc.getApplication(req.params.applicationId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool application not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const application = await svc.updateApplication(existing.id, req.body);
|
||||
@@ -467,6 +477,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.delete("/tool-applications/:applicationId", async (req, res) => {
|
||||
const existing = await svc.getApplication(req.params.applicationId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool application not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const application = await svc.deleteApplication(existing.id);
|
||||
await logActivity(db, {
|
||||
@@ -516,16 +527,14 @@ export function toolAccessRoutes(
|
||||
router.get("/tool-connections/:connectionId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
if (!hasCompanyAccess(req, connection.companyId)) throw notFound("Tool connection not found");
|
||||
assertCompanyAccess(req, connection.companyId);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
res.json(connection);
|
||||
});
|
||||
|
||||
router.get("/tool-connections/:connectionId/installs", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
if (!hasCompanyAccess(req, connection.companyId)) throw notFound("Tool connection not found");
|
||||
assertCompanyAccess(req, connection.companyId);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
res.json({ connectionId: connection.id, installs: connection.installs ?? [] });
|
||||
});
|
||||
|
||||
@@ -535,6 +544,7 @@ export function toolAccessRoutes(
|
||||
async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
await assertBoardToolPermission(req, connection.companyId, "tools:manage_connections");
|
||||
const snapshot = await svc.putConnectionInstalls(connection.id, req.body, getActorInfo(req));
|
||||
await logActivity(db, {
|
||||
@@ -559,6 +569,7 @@ export function toolAccessRoutes(
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
const rows = await db
|
||||
.select({
|
||||
@@ -596,6 +607,7 @@ export function toolAccessRoutes(
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
await assertCanTestAsAgent(req, connection.companyId, req.body.agentId);
|
||||
try {
|
||||
@@ -620,6 +632,7 @@ export function toolAccessRoutes(
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, connection.companyId, "Tool connection not found");
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
try {
|
||||
const status = await options.toolGateway.getTestCallStatus({
|
||||
@@ -635,6 +648,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.patch("/tool-connections/:connectionId", validate(updateToolConnectionSchema), async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const connection = await svc.updateConnection(existing.id, req.body);
|
||||
const lifecycleChanges = classifyConnectionUpdate(
|
||||
@@ -678,6 +692,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.delete("/tool-connections/:connectionId", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const applicationBefore = await svc.getApplication(existing.applicationId);
|
||||
const connection = await svc.archiveConnection(existing.id);
|
||||
@@ -707,6 +722,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tool-connections/:connectionId/health-check", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
res.json(await svc.checkHealth(existing.id, getActorInfo(req)));
|
||||
});
|
||||
@@ -716,6 +732,7 @@ export function toolAccessRoutes(
|
||||
validate(reconnectToolAppSchema),
|
||||
async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.reconnectGalleryApp(
|
||||
existing.id,
|
||||
@@ -738,6 +755,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tool-connections/:connectionId/catalog/refresh", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
res.json(await svc.refreshCatalog(existing.id, getActorInfo(req)));
|
||||
});
|
||||
@@ -745,16 +763,14 @@ export function toolAccessRoutes(
|
||||
router.get("/tool-connections/:connectionId/catalog", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
if (!hasCompanyAccess(req, existing.companyId)) throw notFound("Tool connection not found");
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
res.json({ catalog: await svc.listCatalog(existing.id, existing.companyId) });
|
||||
});
|
||||
|
||||
router.get("/tool-connections/:connectionId/activity", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
if (!hasCompanyAccess(req, existing.companyId)) throw notFound("Tool connection not found");
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool connection not found");
|
||||
const limitRaw = Number(req.query.limit ?? 20);
|
||||
const limit = Number.isFinite(limitRaw) ? limitRaw : 20;
|
||||
res.json(await svc.listConnectionActivity(existing.id, existing.companyId, limit));
|
||||
@@ -770,8 +786,7 @@ export function toolAccessRoutes(
|
||||
router.get("/tool-profiles/:profileId/new-tools", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
if (!hasCompanyAccess(req, existing.companyId)) throw notFound("Tool profile not found");
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
res.json(await svc.listProfileNewTools(existing.id, existing.companyId));
|
||||
});
|
||||
|
||||
@@ -804,6 +819,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.patch("/tool-profiles/:profileId", validate(updateToolProfileWithEntriesSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const profile = await svc.updateProfile(existing.id, req.body);
|
||||
@@ -824,6 +840,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tool-profiles/:profileId/duplicate", validate(duplicateToolProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const profile = await svc.duplicateProfile(existing.id, req.body);
|
||||
@@ -849,6 +866,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.delete("/tool-profiles/:profileId", validate(deleteToolProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.deleteProfile(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
@@ -870,6 +888,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tool-profiles/:profileId/new-tools/review", validate(reviewToolProfileNewToolsSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.reviewProfileNewTools(existing.id, req.body, getActorInfo(req));
|
||||
await logActivity(db, {
|
||||
@@ -890,6 +909,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.post("/tool-profiles/:profileId/entries", validate(createToolProfileEntryForProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.addProfileEntry(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
@@ -906,6 +926,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.patch("/tool-profile-entries/:entryId", validate(updateToolProfileEntrySchema), async (req, res) => {
|
||||
const existing = await svc.getProfileEntry(req.params.entryId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile entry not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.updateProfileEntry(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
@@ -922,6 +943,7 @@ export function toolAccessRoutes(
|
||||
|
||||
router.delete("/tool-profile-entries/:entryId", async (req, res) => {
|
||||
const existing = await svc.getProfileEntry(req.params.entryId as string);
|
||||
assertLookedUpToolResourceAccess(req, existing.companyId, "Tool profile entry not found");
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.deleteProfileEntry(existing.id);
|
||||
await logActivity(db, {
|
||||
|
||||
Reference in new issue
Block a user