fix(server): close tool-access cross-tenant ID oracles (#9589)
## Thinking Path > - Paperclip's company-scoped HTTP routes must reject inaccessible resources before returning resource-specific authorization results. > - The shared `getAccessibleResource` helper established that invariant, but direct tool-access routes still fetched globally unique IDs first and then returned 403 from later authorization checks. > - A signed-in user could therefore distinguish a valid foreign-company resource ID from an unknown ID. > - This change applies the existing tenant-aware lookup gate consistently across direct tool-resource routes and rejects inaccessible OAuth state before callback-specific authorization. ## Linked Issues or Issue Description - No standalone issue exists. This is a security-hardening follow-up to #3967. - **Observed:** a member of company A can submit a known application, connection, profile, profile-entry, or OAuth-state ID belonging to company B and receive a different response than for a random missing ID. - **Expected:** missing and inaccessible foreign resources are indistinguishable at the HTTP boundary. Signed-in instance administrators still require company membership for company-scoped access. - **Reproduction:** create resources in company B, authenticate as an owner of company A without B membership, and call the direct `/api/tool-*` routes using B's IDs. Before this change, affected calls returned 403 while unknown IDs returned 404. ## What Changed - Wrapped direct application, connection, profile, and profile-entry lookups in `server/src/routes/tool-access.ts` with the shared `getAccessibleResource` 404 gate. - Added tenant membership validation to OAuth callback-state lookup before session/role checks, returning the same invalid-state response as an unknown state. - Expanded route regressions across connection/profile endpoint families, including grants, usage, installs, gateway-backed test calls, OAuth, mutations, catalog/activity reads, profile entries, and instance-admin-without-membership access. - Updated application update/delete expectations from cross-tenant 403 to non-enumerating 404 responses. ## Verification After rebasing onto current `master`: - `pnpm exec vitest run src/__tests__/tool-access-service.test.ts` from `server/` — 113 passed. - `pnpm --filter @paperclipai/server typecheck` — previously passed on the same implementation; affected upstream paths were unchanged before this mechanical rebase. ## Risks - Low implementation risk: no schema, migration, or successful same-company response changes. - Intentional behavior change: inaccessible foreign tool-resource IDs now return 404 instead of 403; inaccessible OAuth states return the same 400 body as missing/expired states. - The gate reuses `getAccessibleResource` / `hasCompanyAccess` semantics established by #3967. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, exact model ID `openai-codex/gpt-5.6-sol`; repository, shell, test, TypeScript language-server, and GitHub CLI tool access enabled. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either linked an existing issue or described the issue in-PR - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket ID - [x] I have run focused tests locally on the final rebased head and they pass - [x] I have added or updated tests where applicable - [x] Documentation update — N/A: internal authorization correction only - [x] I have considered and documented risks above - [ ] All Paperclip CI gates are green on the new rebased head - [x] Greptile's prior review was 5/5 with no open findings - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Daniel Sauer <sauerdaniel@users.noreply.github.com>
This commit is contained in:
parent
1f7959bc69
commit
91669741d2
|
|
@ -2208,14 +2208,20 @@ 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 profile routes and missing profiles", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const profile = await toolAccessService(db).createProfile(otherCompany.id, {
|
||||
const service = toolAccessService(db);
|
||||
const profile = await service.createProfile(otherCompany.id, {
|
||||
profileKey: `other-profile-${randomUUID()}`,
|
||||
name: "Other company profile",
|
||||
defaultAction: "deny",
|
||||
});
|
||||
const entry = await service.addProfileEntry(profile.id, {
|
||||
selectorType: "tool_name",
|
||||
effect: "include",
|
||||
toolName: "read_notes",
|
||||
});
|
||||
const app = createRouteApp(db, {
|
||||
type: "board",
|
||||
userId: "member-user",
|
||||
|
|
@ -2233,33 +2239,123 @@ describeEmbeddedPostgres("tool access service", () => {
|
|||
source: "session",
|
||||
});
|
||||
|
||||
await request(app).get(`/api/tool-profiles/${profile.id}/new-tools`).expect(404);
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/duplicate`)
|
||||
.send({ name: "Forbidden copy", includeAssignments: false })
|
||||
.expect(403);
|
||||
await request(app)
|
||||
.delete(`/api/tool-profiles/${profile.id}`)
|
||||
.send({ force: false })
|
||||
.expect(403);
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/new-tools/review`)
|
||||
.send({ decisions: [{ catalogEntryId: randomUUID(), decision: "keep_blocked" }] })
|
||||
.expect(403);
|
||||
const crossTenantResponses = [
|
||||
await request(app).get(`/api/tool-profiles/${profile.id}/new-tools`),
|
||||
await request(app)
|
||||
.patch(`/api/tool-profiles/${profile.id}`)
|
||||
.send({ name: "Cross-tenant edit" }),
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/duplicate`)
|
||||
.send({ name: "Copy" }),
|
||||
await request(app)
|
||||
.delete(`/api/tool-profiles/${profile.id}`)
|
||||
.send({}),
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/new-tools/review`)
|
||||
.send({ decisions: [{ catalogEntryId: randomUUID(), decision: "keep_blocked" }] }),
|
||||
await request(app)
|
||||
.post(`/api/tool-profiles/${profile.id}/entries`)
|
||||
.send({ selectorType: "tool_name", effect: "include", toolName: "write_notes" }),
|
||||
await request(app)
|
||||
.patch(`/api/tool-profile-entries/${entry.id}`)
|
||||
.send({ effect: "exclude" }),
|
||||
await request(app).delete(`/api/tool-profile-entries/${entry.id}`),
|
||||
];
|
||||
const missingRes = await request(app).get(`/api/tool-profiles/${randomUUID()}/new-tools`);
|
||||
|
||||
await request(createRouteApp(db)).get(`/api/tool-profiles/${randomUUID()}/new-tools`).expect(404);
|
||||
await request(createRouteApp(db))
|
||||
.post(`/api/tool-profiles/${randomUUID()}/duplicate`)
|
||||
.send({ name: "Missing copy", includeAssignments: false })
|
||||
.expect(404);
|
||||
await request(createRouteApp(db))
|
||||
.delete(`/api/tool-profiles/${randomUUID()}`)
|
||||
.send({ force: false })
|
||||
.expect(404);
|
||||
await request(createRouteApp(db))
|
||||
.post(`/api/tool-profiles/${randomUUID()}/new-tools/review`)
|
||||
.send({ decisions: [{ catalogEntryId: randomUUID(), decision: "keep_blocked" }] })
|
||||
.expect(404);
|
||||
expect(crossTenantResponses.map((response) => response.status)).toEqual(
|
||||
crossTenantResponses.map(() => 404),
|
||||
);
|
||||
expect(missingRes.status).toBe(404);
|
||||
expect(crossTenantResponses[0]!.body).toEqual(missingRes.body);
|
||||
});
|
||||
|
||||
it("returns 404 for cross-company connection routes, including instance admins", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const service = toolAccessService(db);
|
||||
const connection = await service.createConnection(otherCompany.id, {
|
||||
name: "Other company connection",
|
||||
transport: "mcp_remote",
|
||||
config: { url: "https://other-company.example/mcp" },
|
||||
});
|
||||
const oauthState = randomUUID();
|
||||
await db.insert(toolOauthStates).values({
|
||||
state: oauthState,
|
||||
companyId: otherCompany.id,
|
||||
connectionId: connection.id,
|
||||
codeVerifier: "cross-tenant-code-verifier",
|
||||
createdByActorType: "user",
|
||||
createdByActorId: "other-user",
|
||||
createdBySessionId: null,
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
});
|
||||
const toolGateway = {} as ToolGatewayService;
|
||||
const app = createRouteApp(db, {
|
||||
type: "board",
|
||||
userId: "member-user",
|
||||
userName: "Member User",
|
||||
userEmail: null,
|
||||
companyIds: [allowedCompany.id],
|
||||
memberships: [
|
||||
{
|
||||
companyId: allowedCompany.id,
|
||||
membershipRole: "owner",
|
||||
status: "active",
|
||||
},
|
||||
],
|
||||
isInstanceAdmin: true,
|
||||
source: "session",
|
||||
}, toolGateway);
|
||||
const foreignOAuthRes = await request(app)
|
||||
.get("/api/tools/oauth/callback")
|
||||
.query({ state: oauthState, code: "oauth-code" });
|
||||
const missingOAuthRes = await request(app)
|
||||
.get("/api/tools/oauth/callback")
|
||||
.query({ state: randomUUID(), code: "oauth-code" });
|
||||
|
||||
const crossTenantResponses = [
|
||||
await request(app).post(`/api/tools/oauth/${connection.id}/start`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/grants`),
|
||||
await request(app)
|
||||
.post(`/api/tool-connections/${connection.id}/grants/installations`)
|
||||
.send({}),
|
||||
await request(app).delete(`/api/tool-connections/${connection.id}/grants/${randomUUID()}`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/usage`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/installs`),
|
||||
await request(app)
|
||||
.put(`/api/tool-connections/${connection.id}/installs`)
|
||||
.send({ installs: [] }),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/test-agents`),
|
||||
await request(app)
|
||||
.post(`/api/tool-connections/${connection.id}/test-calls`)
|
||||
.send({ agentId: randomUUID(), toolName: "read_notes", parameters: {} }),
|
||||
await request(app)
|
||||
.get(`/api/tool-connections/${connection.id}/test-calls/${randomUUID()}`),
|
||||
await request(app)
|
||||
.patch(`/api/tool-connections/${connection.id}`)
|
||||
.send({ name: "Cross-tenant edit" }),
|
||||
await request(app).delete(`/api/tool-connections/${connection.id}`),
|
||||
await request(app).post(`/api/tool-connections/${connection.id}/health-check`),
|
||||
await request(app)
|
||||
.post(`/api/tool-connections/${connection.id}/reconnect`)
|
||||
.send({ credentialValues: {} }),
|
||||
await request(app).post(`/api/tool-connections/${connection.id}/catalog/refresh`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/catalog`),
|
||||
await request(app).get(`/api/tool-connections/${connection.id}/activity?limit=5`),
|
||||
];
|
||||
expect(foreignOAuthRes.status).toBe(400);
|
||||
expect(missingOAuthRes.status).toBe(400);
|
||||
expect(foreignOAuthRes.body).toEqual(missingOAuthRes.body);
|
||||
const missingRes = await request(app).get(`/api/tool-connections/${randomUUID()}`);
|
||||
|
||||
for (const response of crossTenantResponses) {
|
||||
expect(response.status).toBe(404);
|
||||
}
|
||||
expect(missingRes.status).toBe(404);
|
||||
expect(crossTenantResponses[1]!.body).toEqual(missingRes.body);
|
||||
await expect(service.getConnection(connection.id)).resolves.toMatchObject({ id: connection.id });
|
||||
});
|
||||
|
||||
it("installs the safe example fixture idempotently and smokes allow, deny, and audit paths", async () => {
|
||||
|
|
@ -5604,7 +5700,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 application updates and missing applications", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const application = await toolAccessService(db).createApplication(otherCompany.id, {
|
||||
|
|
@ -5635,7 +5731,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
|||
.patch(`/api/tool-applications/${randomUUID()}`)
|
||||
.send({ name: "Missing edit" });
|
||||
|
||||
expect(forbiddenRes.status).toBe(403);
|
||||
expect(forbiddenRes.status).toBe(404);
|
||||
expect(missingRes.status).toBe(404);
|
||||
});
|
||||
|
||||
|
|
@ -5990,7 +6086,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 application deletes and missing applications", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const application = await toolAccessService(db).createApplication(otherCompany.id, {
|
||||
|
|
@ -6017,7 +6113,7 @@ describeEmbeddedPostgres("tool access service", () => {
|
|||
const forbiddenRes = 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(forbiddenRes.status).toBe(404);
|
||||
expect(missingRes.status).toBe(404);
|
||||
const stillThere = await db
|
||||
.select()
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ import {
|
|||
updateToolProfileWithEntriesSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { getActorInfo, assertBoard, assertCompanyAccess, hasCompanyAccess } from "./authz.js";
|
||||
import { badRequest, forbidden, notFound, unprocessable } from "../errors.js";
|
||||
import { getActorInfo, assertBoard, assertCompanyAccess, getAccessibleResource, hasCompanyAccess } from "./authz.js";
|
||||
import { badRequest, forbidden, unprocessable } from "../errors.js";
|
||||
import { accessService, googleSheetsRobotEmailFromEnv, logActivity, toolAccessPolicyService, toolAccessService } from "../services/index.js";
|
||||
import { ToolGatewayHttpError, type ToolGatewayService } from "../services/tool-gateway.js";
|
||||
|
||||
|
|
@ -308,7 +308,8 @@ export function toolAccessRoutes(
|
|||
);
|
||||
|
||||
router.post("/tools/oauth/:connectionId/start", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.startOAuth(existing.companyId, existing.id, {
|
||||
redirectUri: oauthRedirectUri(),
|
||||
|
|
@ -324,7 +325,7 @@ export function toolAccessRoutes(
|
|||
const error = typeof req.query.error === "string" ? req.query.error : null;
|
||||
const errorDescription = typeof req.query.error_description === "string" ? req.query.error_description : null;
|
||||
const pendingState = state ? await svc.peekOAuthState(state) : null;
|
||||
if (!pendingState) {
|
||||
if (!pendingState || !hasCompanyAccess(req, pendingState.companyId)) {
|
||||
throw badRequest("Invalid or expired OAuth state");
|
||||
}
|
||||
assertToolAppMutationAccess(req, pendingState.companyId);
|
||||
|
|
@ -489,7 +490,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.patch("/tool-applications/:applicationId", validate(updateToolApplicationSchema), async (req, res) => {
|
||||
const existing = await svc.getApplication(req.params.applicationId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getApplication(req.params.applicationId as string), "Tool application not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const application = await svc.updateApplication(existing.id, req.body);
|
||||
|
|
@ -509,7 +511,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.delete("/tool-applications/:applicationId", async (req, res) => {
|
||||
const existing = await svc.getApplication(req.params.applicationId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getApplication(req.params.applicationId as string), "Tool application not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const application = await svc.deleteApplication(existing.id);
|
||||
await logActivity(db, {
|
||||
|
|
@ -558,23 +561,22 @@ 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);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
res.json(connection);
|
||||
});
|
||||
|
||||
router.get("/tool-connections/:connectionId/grants", 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);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
res.json(await svc.listConnectionGrants(connection.id, connection.companyId));
|
||||
});
|
||||
|
||||
router.post("/tool-connections/:connectionId/grants/installations", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardToolPermission(req, connection.companyId, "tools:manage_connections");
|
||||
const body = req.body && typeof req.body === "object" ? req.body as Record<string, unknown> : {};
|
||||
const credentialSecretRefs = Array.isArray(body.credentialSecretRefs) ? body.credentialSecretRefs : [];
|
||||
|
|
@ -600,7 +602,8 @@ export function toolAccessRoutes(
|
|||
|
||||
router.delete("/tool-connections/:connectionId/grants/:grantId", async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardToolPermission(req, connection.companyId, "tools:manage_connections");
|
||||
const grant = await svc.revokeConnectionGrant(connection.id, req.params.grantId as string, getActorInfo(req));
|
||||
await logActivity(db, {
|
||||
|
|
@ -617,9 +620,8 @@ export function toolAccessRoutes(
|
|||
|
||||
router.get("/tool-connections/:connectionId/usage", 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);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
const range = req.query.range === "30d" ? "30d" : req.query.range === undefined || req.query.range === "7d" ? "7d" : null;
|
||||
if (!range) throw badRequest("Usage range must be 7d or 30d");
|
||||
res.json(await svc.getConnectionUsage(connection.id, range, connection.companyId));
|
||||
|
|
@ -627,9 +629,8 @@ export function toolAccessRoutes(
|
|||
|
||||
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);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
res.json({ connectionId: connection.id, installs: connection.installs ?? [] });
|
||||
});
|
||||
|
||||
|
|
@ -638,7 +639,8 @@ export function toolAccessRoutes(
|
|||
validate(putToolConnectionInstallsSchema),
|
||||
async (req, res) => {
|
||||
assertBoard(req);
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardToolPermission(req, connection.companyId, "tools:manage_connections");
|
||||
const snapshot = await svc.putConnectionInstalls(connection.id, req.body, getActorInfo(req));
|
||||
await logActivity(db, {
|
||||
|
|
@ -662,7 +664,8 @@ export function toolAccessRoutes(
|
|||
res.status(501).json({ error: "Tool gateway service is not configured" });
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
const rows = await db
|
||||
.select({
|
||||
|
|
@ -699,7 +702,8 @@ export function toolAccessRoutes(
|
|||
res.status(501).json({ error: "Tool gateway service is not configured" });
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
await assertCanTestAsAgent(req, connection.companyId, req.body.agentId);
|
||||
try {
|
||||
|
|
@ -723,7 +727,8 @@ export function toolAccessRoutes(
|
|||
res.status(501).json({ error: "Tool gateway service is not configured" });
|
||||
return;
|
||||
}
|
||||
const connection = await svc.getConnection(req.params.connectionId as string);
|
||||
const connection = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!connection) return;
|
||||
await assertBoardAnyToolPermission(req, connection.companyId, ["tools:use", "tools:manage_connections"]);
|
||||
try {
|
||||
const status = await options.toolGateway.getTestCallStatus({
|
||||
|
|
@ -738,7 +743,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.patch("/tool-connections/:connectionId", validate(updateToolConnectionSchema), async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const connection = await svc.updateConnection(existing.id, req.body);
|
||||
const lifecycleChanges = classifyConnectionUpdate(
|
||||
|
|
@ -781,7 +787,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.delete("/tool-connections/:connectionId", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const applicationBefore = await svc.getApplication(existing.applicationId);
|
||||
const connection = await svc.archiveConnection(existing.id);
|
||||
|
|
@ -810,7 +817,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.post("/tool-connections/:connectionId/health-check", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
res.json(await svc.checkHealth(existing.id, getActorInfo(req)));
|
||||
});
|
||||
|
|
@ -819,7 +827,8 @@ export function toolAccessRoutes(
|
|||
"/tool-connections/:connectionId/reconnect",
|
||||
validate(reconnectToolAppSchema),
|
||||
async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.reconnectGalleryApp(
|
||||
existing.id,
|
||||
|
|
@ -841,24 +850,23 @@ export function toolAccessRoutes(
|
|||
);
|
||||
|
||||
router.post("/tool-connections/:connectionId/catalog/refresh", async (req, res) => {
|
||||
const existing = await svc.getConnection(req.params.connectionId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
res.json(await svc.refreshCatalog(existing.id, getActorInfo(req)));
|
||||
});
|
||||
|
||||
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);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
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);
|
||||
const existing = await getAccessibleResource(req, res, svc.getConnection(req.params.connectionId as string), "Tool connection not found");
|
||||
if (!existing) return;
|
||||
const limitRaw = Number(req.query.limit ?? 20);
|
||||
const limit = Number.isFinite(limitRaw) ? limitRaw : 20;
|
||||
res.json(await svc.listConnectionActivity(existing.id, existing.companyId, limit));
|
||||
|
|
@ -873,9 +881,8 @@ 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);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
res.json(await svc.listProfileNewTools(existing.id, existing.companyId));
|
||||
});
|
||||
|
||||
|
|
@ -907,7 +914,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.patch("/tool-profiles/:profileId", validate(updateToolProfileWithEntriesSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const profile = await svc.updateProfile(existing.id, req.body);
|
||||
|
|
@ -927,7 +935,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.post("/tool-profiles/:profileId/duplicate", validate(duplicateToolProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
try {
|
||||
const profile = await svc.duplicateProfile(existing.id, req.body);
|
||||
|
|
@ -952,7 +961,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.delete("/tool-profiles/:profileId", validate(deleteToolProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.deleteProfile(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
|
|
@ -973,7 +983,8 @@ 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);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const result = await svc.reviewProfileNewTools(existing.id, req.body, getActorInfo(req));
|
||||
await logActivity(db, {
|
||||
|
|
@ -993,7 +1004,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.post("/tool-profiles/:profileId/entries", validate(createToolProfileEntryForProfileSchema), async (req, res) => {
|
||||
const existing = await svc.getProfile(req.params.profileId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfile(req.params.profileId as string), "Tool profile not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.addProfileEntry(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
|
|
@ -1009,7 +1021,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.patch("/tool-profile-entries/:entryId", validate(updateToolProfileEntrySchema), async (req, res) => {
|
||||
const existing = await svc.getProfileEntry(req.params.entryId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfileEntry(req.params.entryId as string), "Tool profile entry not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.updateProfileEntry(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
|
|
@ -1025,7 +1038,8 @@ export function toolAccessRoutes(
|
|||
});
|
||||
|
||||
router.delete("/tool-profile-entries/:entryId", async (req, res) => {
|
||||
const existing = await svc.getProfileEntry(req.params.entryId as string);
|
||||
const existing = await getAccessibleResource(req, res, svc.getProfileEntry(req.params.entryId as string), "Tool profile entry not found");
|
||||
if (!existing) return;
|
||||
assertToolAppMutationAccess(req, existing.companyId);
|
||||
const entry = await svc.deleteProfileEntry(existing.id);
|
||||
await logActivity(db, {
|
||||
|
|
|
|||
Loading…
Reference in New Issue