Skip to content

Show restricted members only their own calls in the audit API - #1665

Merged
helpcodeai merged 1 commit into
mainfrom
feat/member-visibility-audit
Nov 13, 2026
Merged

helpcodeai merged 1 commit into
mainfrom
feat/member-visibility-audit

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

Second step of member visibility (after #1664). In a workspace set to restricted, a member who is not an admin now gets only the tool calls they made themselves from every audit read route. Admins and open workspaces keep today's code path and today's queries.

Cloud and self-hosted: identical, off by default. No workspace has the setting yet, so nothing changes on deploy.

Changes

  • AuditController: each GET route asks MemberVisibilityService.scopeFor(req.user) first. Not restricted: today's call, unchanged (same arguments, same arity). Restricted: the same call plus the caller's user id.
    • GET /api/audit/invocations: only rows with userId = caller. Rows without a user are hidden. Same response shape. toolId, connectorId, mcpServerId, search, status, limit, offset combine with the user filter (AND).
    • GET /api/audit/stats: the four counters count only the caller's calls.
    • GET /api/audit/analytics: AND ti.user_id = $n in analyticsQuery.
    • GET /api/audit/tool-performance: AND ti.user_id = $n in toolPerformanceQuery, both in the grouped page and in the latest-error lookup. Without the second one, another member's newer error text on the same tool would show as lastError.
    • GET /api/audit/breakdowns: every list and total counts only the caller's calls; byUser holds only their own row.
  • AuditService / tool-performance.query.ts: optional onlyUserId. Prisma where or a bound SQL value, never string concatenation. The organizationId filter stays in every query. Absent on the open path; when present it is always applied, so an empty value matches nothing.
  • No cache to split: the audit reads have no per-workspace cache. Only the visibility mode is cached (30 s, in MemberVisibilityService, from Add the member visibility setting and the service that decides it #1664).
  • AuditModule imports MemberVisibilityModule (not SettingsModule, which the File Search worker loads without roles and audit).
  • OpenAPI: one sentence on each of the five operations. packages/cli/src/generated/operations.ts does not change (it carries no descriptions).
  • Fail closed: if the mode or role lookup throws for a member, the route answers 500 and AuditService is not called.

Type

  • Bug fix
  • New feature
  • Documentation
  • Refactoring
  • Performance improvement
  • Other (describe below)

Testing

  • Added/updated unit tests
  • Tested manually (describe steps below)
  • No tests needed (explain why)

New specs:

  • audit.controller.spec.ts: matrix for every GET route, ADMIN / EDITOR / VIEWER x open / never set / restricted, with and without a tool whitelist. Asserts exact AuditService arguments. Includes a route coverage check (fails if a GET route has no rule) and fail-closed cases.
  • audit.member-visibility.spec.ts: Prisma where and SQL text/values with and without the user filter; injection attempt stays a bound value.
  • audit.member-visibility.integration.spec.ts (real migrated PostgreSQL, AUDIT_TEST_DATABASE_URL, as in CI): HTTP to the five routes with real memberships, real org_settings mode and the real RolesService. Open: everyone sees all. Restricted: admin sees all; viewer and editor see only their rows; no filter parameter returns another member's row; stats, analytics, tool performance (including lastError) and breakdowns hold only own calls; 500 when the role lookup throws. Cleans up its rows.
  • audit.module.spec.ts: AuditController gets the real MemberVisibilityService through MemberVisibilityModule.

Existing specs: no expectation changed. tool-performance.integration.spec.ts got one provider in its setup (MemberVisibilityService returning open), because the controller now injects it.

Commands, run after rebasing on main with #1664 merged (in packages/backend, local PostgreSQL 17 on port 5433, database migrated with npx prisma migrate deploy):

  • AUDIT_TEST_DATABASE_URL=... npx jest src/audit src/settings src/roles --maxWorkers=4: 31 suites passed, 1 failed; 1531 tests passed, 1 failed.
    • The failure is a baseline: tool-performance.integration.spec.ts "weights ERROR and TIMEOUT counts" expects 285 and gets 284.99999999999994 from local PostgreSQL 17.11 on arm64. It fails the same way on the base branch without this change.
  • AUDIT_TEST_DATABASE_URL=... npx jest src/audit/audit.member-visibility.integration --maxWorkers=4: 16 passed.
  • npx jest src/audit --maxWorkers=4 (no database): 16 suites passed, 2 skipped (the PostgreSQL specs); 293 tests passed, 25 skipped.
  • Mutation check: removing the user filter from the latest-error lookup makes 3 of the new tests fail.
  • npx eslint <changed files>: no findings.
  • npx tsc --noEmit -p tsconfig.json: no errors.
  • node scripts/generate-cli-operations.mjs (repo root): no diff.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable) (docs come with the frontend PR)
  • All existing tests pass (npm test) (ran the audit, settings and roles suites; one baseline failure above)
  • This PR has a descriptive title

Related Issues

Relates to #1664

@helpcodeai
helpcodeai merged commit 45e0fdf into main Nov 13, 2026
24 of 25 checks passed
@helpcodeai
helpcodeai deleted the feat/member-visibility-audit branch November 13, 2026 17:12
@github-actions github-actions Bot locked and limited conversation to collaborators Nov 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant