All documentation

Security

Permission Matrix

Module 9 introduces workspace roles beyond Module 1's original OWNER/ADMIN/MEMBER pair. This document is the authoritative reference for what each role can do — enforced in code by apps/api/src/modules/collaboration/permissions.util.ts.

Role hierarchy

Roles form a single total order (a simple rank, not a per-action ACL table) — see ROLE_RANK in permissions.util.ts. A role can do everything a lower-ranked role can do, plus whatever is gated at its own rank.

RankRoleOrigin
6OWNERModule 1
5ADMINModule 1
4MANAGERModule 9
3TRIAGERModule 9
2REVIEWERModule 9
1MEMBERModule 1

requireRole(role, minimum) throws INSUFFICIENT_WORKSPACE_ROLE (403) if the caller's rank is below minimum. requireCanGrantRole(actingRole, targetRole) additionally stops a member from inviting or promoting someone into a role ranked above their own (a TRIAGER can never grant ADMIN, for instance).

Action → minimum role

ActionMinimum roleEnforced in
View programs, findings, reports, dashboard, analyticsMEMBERAll BugBounty read queries (workspace membership only, no role floor)
Create/update/comment/assign findings, reports, tasksMEMBERbugbounty-finding-lifecycle.commands.ts, comments.commands.ts, assignments.commands.ts, task-items.commands.ts
Review/triage a report, change finding severityREVIEWERReport/finding review-oriented endpoints (role floor documented per-handler)
Transition finding stage, submit/triage reportsTRIAGERbugbounty-finding-lifecycle.commands.ts (TransitionBugBountyFindingStageHandler), bug-report-lifecycle.commands.ts
Create/update/delete a Scheduled Job, run it manuallyMANAGERautomation/commands/scheduled-jobs.commands.ts
Create/update/delete a Webhook Token, test deliveryMANAGERpublic-api/commands/webhook-tokens.commands.ts
Create a workspace API keyMANAGERpublic-api/commands/api-keys.commands.ts (CreateApiKeyHandler)
Revoke a workspace API keyADMINpublic-api/commands/api-keys.commands.ts (RevokeApiKeyHandler)
Invite a member, revoke an invite, list invitesADMINcollaboration/commands/workspace-invites.commands.ts, collaboration/queries/workspace-invites.query.ts
Change a member's roleADMIN, and only to a role ≤ the actor's ownUpdateWorkspaceMemberRoleHandler (requireRole + requireCanGrantRole)
Remove a memberADMINRemoveWorkspaceMemberHandler
Demote the workspace's only OWNERBlocked outright (409 WORKSPACE_MEMBER_ROLE_CANNOT_DEMOTE_LAST_OWNER), regardless of roleUpdateWorkspaceMemberRoleHandler

Personal-workspace-scoped resources (Programs, Findings, Reports, Knowledge Base, Payload Library, Notifications, Bookmarks, Calendar — everything resolved via resolveWorkspaceForCaller in bugbounty/authorization.util.ts) have no per-action role floor beyond "is a member of this workspace" today, since a personal workspace has exactly one member. The MANAGER/TRIAGER/ REVIEWER ranks exist for the explicit-multi-member-workspace surface (Collaboration, Automation, Notification channels, Public API — everything resolved via resolveWorkspaceMembership in collaboration/workspace-membership.util.ts), which is where a real team shares a workspace and role separation matters.

Audit trail

Every role-gated mutation in the table above that changes workspace membership or credentials also writes an AuditEvent row (AuditEventType, Prisma-schema enum, see schema.prisma):

  • WORKSPACE_MEMBER_INVITED / WORKSPACE_MEMBER_ROLE_CHANGED / WORKSPACE_MEMBER_REMOVED — workspace-invites.commands.ts
  • API_KEY_CREATED / API_KEY_REVOKED — public-api/commands/api-keys.commands.ts
  • WEBHOOK_TOKEN_CREATED / WEBHOOK_TOKEN_REVOKED — public-api/commands/webhook-tokens.commands.ts

These CQRS command handlers run outside an HTTP request context, so ipAddress/userAgent on the audit row are recorded as placeholder values ("0.0.0.0" / "api") rather than the caller's real network origin — the same limitation already accepted for the pre-existing WEBHOOK_TOKEN_* events. Threading real request metadata into CQRS commands would require a request-scoped context object across every controller → command boundary; noted as a follow-up, not a blocker.

Module 1's original account-security events (LOGIN_SUCCESS, MFA_CHALLENGE_FAILURE, SESSION_REVOKED, etc.) are unaffected — see docs/adr/0001-auth-module.md.

Session & Device History

Session/device history (list active sessions with device name, IP, browser, OS, last-active time; revoke a session) is Module 1 functionality, unchanged by Module 9 — see Session/RefreshToken in schema.prisma and apps/api/src/modules/auth/controllers/sessions.controller.ts. Nothing in Module 9 required extending it: workspace roles are orthogonal to device sessions, which are per-user, not per-workspace.

Secrets at rest

WebhookToken.secret and NotificationChannelConfig.config (webhook URLs, bot tokens, custom-API endpoints/headers) are encrypted at rest with AES-256-GCM via the same common/utils/credential-encryption.util.ts service used for AiProviderCredential.encryptedApiKey (Phase 1 BYOK), keyed off the CREDENTIAL_ENCRYPTION_KEY environment variable — see notification-channel-config-crypto.util.ts for the per-field encrypt/ decrypt helpers and webhook-tokens.commands.ts for the webhook-secret path. Neither is ever returned to a client in plaintext (webhook secrets aren't returned at all past creation-time HMAC use; channel-config secret fields are redacted to "***" in every DTO response — see to-channel-config-dto.mapper.ts).