Skip to main content

Roles & Permissions

Team / org roles

These roles are assigned per Member record (user in a team):

RoleLevelCapabilities
SUPERADMINPlatformPlatform-wide admin
OWNEROrgFull control, billing, delete org
ADMINOrgUser management, template configuration, all applications
MANAGERWorkspaceApplication management, document review, client management
MEMBERTeamProcess applications, add comments
CLIENTWorkspaceSee own applications + EXTERNAL comments only

Per-application roles (Applicant roles)

Each Applicant on an application has an independent role:

RoleCapabilities
VIEWERRead-only access to the application
COMMENTERCan add comments
EDITORCan edit application details

These are separate from the workspace-level team roles.

Cascading permissions

Permissions cascade through the hierarchy: team → workspace → org.

A user with a role on an org workspace implicitly has that role on all teams within that workspace. The evaluator checks: team role → workspace role → org role.

Custom evaluator: AxveroRolePermissionEvaluator (backend)

  • hasUserAnyOrgRole(id, roles...) — org-level check
  • hasUserAnyTeamRole(id, roles...) — team-level check
  • hasUserAnyRoleForTeam(id, roles...) — cascading check (team → workspace → org)

Data scoping by role

This is a critical business rule — what each role sees:

DataStaff (OWNER/ADMIN/MANAGER/MEMBER)Client (CLIENT)
ApplicationsAll applications in workspaceOnly their own applications
CommentsINTERNAL + EXTERNALEXTERNAL only
Applicant detailsAllTheir own only
Client listAll clientsNot accessible

Implementation: In the backend service, resolve currentUserId in the controller and pass to service. Service calls ClientService.findClientIdByUserIdAndWorkspaceId(currentUserId, workspaceId). If a client ID is found, scope results to that client's data.

warning

Never show INTERNAL comments to clients. This is enforced at the GraphQL query level — type filtering happens before returning comment data.

Permission annotations (backend)

Read guards — membership checks (unchanged)

// Any workspace member (read access)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyWorkspaceRole(#workspaceId, 'OWNER','ADMIN','MANAGER','MEMBER','CLIENT')")

// Org-level read
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyOrgRole(#orgId, 'OWNER','ADMIN','MANAGER','MEMBER')")

// Workspace or client role (mixed workspaces)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAnyWorkspaceOrClientRole(#workspaceId, #clientId)")

Write guards — permission matrix checks

Write and mutate operations use the configurable permission matrix instead of hardcoded role lists:

// System permission (org-level operation)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserSystemPermission(#orgId, 'MANAGE_TEAMS')")

// System permission resolved from workspaceId
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserSystemPermission(@workspaceRepository.findOrgIdByWorkspaceId(#workspaceId), 'MANAGE_APPLICATION_SETUP')")

// Application permission (action inside an application)
@PreAuthorize("@axveroRolePermissionEvaluator.hasUserAppPermissionForApplication(#applicationId, 'DECIDE')")

All GraphQL controllers carry @PreAuthorize("isAuthenticated()") at the class level.

Automatic creator membership

When creating entities (teams, workspaces), the creator is automatically added as OWNER member. This must always be implemented when adding new create operations.


See also