Skip to content

Add the member visibility setting and the service that decides it - #1664

Merged
helpcodeai merged 2 commits into
mainfrom
feat/member-visibility-service
Nov 13, 2026
Merged

helpcodeai merged 2 commits into
mainfrom
feat/member-visibility-service

Conversation

@helpcodeai

Copy link
Copy Markdown
Contributor

Summary

First step of "member visibility". A workspace admin will be able to switch the workspace to restricted mode. A member who is not an admin then sees in the dashboard and the REST API only what their AI client can use.

This PR adds the setting, the service that decides it, and two settings routes. No existing endpoint calls the service yet, so no response changes. The later PRs use the service in the audit, connectors, tools, MCP servers and File Search controllers.

Changes

  • MemberVisibilityService in packages/backend/src/settings/ (AGPL, outside ee/). scopeFor(req.user) returns { restricted: false } or { restricted: true, userId, allowedToolIds }.
    • An ADMIN gets { restricted: false } before any database read.
    • The mode is the org_settings key member_visibility, read through OrgSettingsService. No row, or any value other than restricted, means open.
    • The mode is cached per workspace for 30 s in memory. A write in the same process drops it.
    • allowedToolIds comes from RolesService.getAllowedToolIds on every call, never cached. null means the roles narrow nothing.
    • Fails closed: if the setting or the role lookup throws, the error reaches the caller. Failures are not cached.
  • Pure helpers for the later PRs: connectorWhere, canSeeConnector, visibleTools, serverWhere (relations tools and connectors.connector, as in schema.prisma). They are always combined with the organizationId filter by AND.
  • GET /api/settings/member-visibility, any signed-in member: { mode, restrictedForMe }.
  • PUT /api/admin/settings/member-visibility, ADMIN only, body { mode: 'open' | 'restricted' } checked with @IsIn. Always writes the caller's workspace. A read-only management key gets 403 from the existing guard. A change writes the security event MEMBER_VISIBILITY_CHANGED with actor and { from, to }. No change, no write and no event.
  • New event slug in SecurityEvents. It is a string constant, not a Prisma enum, so no migration.
  • Full OpenAPI descriptions with an amcp api example. CLI operations regenerated.
  • MemberVisibilityModule is exported for the later PRs and registered in AppModule.

Why a separate module and controllers instead of SiteSettingsMemberController and SiteSettingsAdminController: the File Search worker imports SettingsModule without RolesModule and AuditModule. A dependency on RolesService or SecurityEventService inside SettingsModule would stop the worker at boot. The routes, tag and guards are the same as planned. RolesModule and AuditModule are @Global, so the new module imports only SettingsModule and creates no cycle. A spec checks that the service stays out of SettingsModule.

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:

  • member-visibility.service.spec.ts: mode parsing, ADMIN short-circuit (a Prisma proxy proves no database access), open and restricted results, null and empty tool lists, no cache for roles, 30 s cache and drop on write, fail closed when roles or the setting throw, and the four helpers (including a check that canSeeConnector agrees with connectorWhere).
  • member-visibility.controller.spec.ts: HTTP level with the real JwtOrManagementKeyGuard, RolesGuard and the ValidationPipe options of main.ts. ADMIN can write and the event is logged. EDITOR and VIEWER get 403. A MANAGEMENT_READ key of an ADMIN gets 403. Invalid values and extra fields get 400. GET works for every role. Also the OpenAPI output.
  • member-visibility.module.spec.ts: module wiring with the services from global modules, imported by a module shaped like AuditModule.

Commands, all in the worktree on origin/main 2b31050:

  • packages/backend: npx jest src/settings src/audit src/main-cookie-parser.spec.ts src/auth/mcp-auth.middleware.spec.ts src/common/openapi.spec.ts --maxWorkers=4: 25 suites passed, 1 skipped; 1308 tests passed, 9 skipped.
  • packages/backend: npx jest src/settings src/audit/security-event.service.spec.ts src/auth/jwt-or-management-key.guard.boot.spec.ts src/auth/roles.guard src/common/openapi.spec.ts src/identity-providers src/roles --maxWorkers=4: 30 suites passed, 1 skipped; 1422 tests passed, 12 skipped.
  • packages/backend: npx jest src/auth/jwt-or-management-key.guard.spec.ts: fails in my environment in beforeAll (prisma.organization.create). It is a real-database spec and the local database is not the CI one. The file and the guard are not changed by this PR. CI runs it against a migrated database.
  • packages/backend: npx eslint on the changed files: 0 errors, 1 old warning (ClassSerializerInterceptor unused in app.module.ts).
  • packages/backend: npx tsc --noEmit -p tsconfig.json: pass.
  • Root: node scripts/generate-cli-operations.mjs (213 operations), then --check: up to date.
  • packages/cli: npm test: 25 passed, 2 skipped, 0 failed.

Cloud and self-hosted: identical, off by default. No workspace has the key, so every workspace reads open. No migration.

Checklist

  • My code follows the existing code style
  • I have tested my changes locally
  • I have updated documentation (if applicable). The docs come with the frontend PR.
  • All existing tests pass (npm test). Only the touched areas and their neighbours were run, see above.
  • This PR has a descriptive title

Related Issues

None.

@helpcodeai
helpcodeai merged commit 8cc1d3d into main Nov 13, 2026
16 checks passed
@helpcodeai
helpcodeai deleted the feat/member-visibility-service branch November 13, 2026 16:53
@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