Skip to content

Security & RBAC ​

SchemaStack implements a multi-level Role-Based Access Control (RBAC) system with permissions at the organisation, workspace, view, and column level.

Roles ​

Organisation Roles ​

RoleScope
OwnerFull access to everything in the organisation. Bypasses all permission checks.
AdminManage workspaces, members, and settings. Inherits Admin on all workspaces.
MemberBasic membership. No inherited workspace access — must be explicitly added.

Defined in OrganisationMembership.Role enum (organisation-persistence).

Workspace Roles ​

RoleInherited View RoleDescription
Workspace AdminView AdminFull workspace management + all view access
Workspace EditorView EditorRead/write data in all views
Workspace ViewerView ViewerRead-only data in all views
Workspace MemberNoneWorkspace access but no inherited view access

Defined in WorkspaceMembership.Role enum (metadata-persistence).

The Workspace Member role is for users who should only access specific views via explicit ViewMembership grants.

View Roles ​

RoleCan DesignCan ReadCan Write
AdminYesYesYes
EditorNoYesYes
ViewerNoYesNo

Defined in ViewMembership.Role enum (metadata-persistence).

Permission Resolution ​

Permissions are resolved by ViewPermissionService (metadata-service). The evaluation order:

  1. Organisation Owner → full access (always)
  2. Private view check → if view is private, only explicit ViewMembership counts
  3. Explicit ViewMembership → view-level role override
  4. Inherited from WorkspaceMembership → workspace role converted to view role
  5. Inherited from Organisation Admin → Admin access
  6. Default deny

A view-level role overrides the workspace-level role. This allows both promotions (Editor → View Admin) and restrictions (Editor → View Viewer).

View Actions ​

Actions are defined in the ViewAction enum (metadata-service):

Design Actions (Admin only) ​

DESIGN_VIEW, ADD_COLUMN, REMOVE_COLUMN, MODIFY_COLUMN, REORDER_COLUMNS, CONFIGURE_VIEW, MANAGE_MEMBERS, CONFIGURE_PERMISSIONS

Data Actions ​

VIEW_DATA (Viewer+), ADD_ROW (Editor+), EDIT_ROW (Editor+), DELETE_ROW (Editor+), EXPORT_DATA (Viewer+)

Bulk Actions (Editor+) ​

BULK_DELETE, BULK_UPDATE, BULK_EXPORT

View Settings Enforcement ​

Data actions for Editor and Viewer roles are gated by view-level boolean flags (addable, editable, exportable). Admin role bypasses all view setting checks.

ActionAdminEditorViewer
VIEW_DATAalwaysalwaysalways
ADD_ROWalwaysrequires addable = truedenied
EDIT_ROWalwaysrequires editable = truedenied
DELETE_ROWalwaysrequires editable = truedenied
EXPORT_DATAalwaysrequires exportable = truerequires exportable = true
BULK_DELETEalwaysrequires editable = truedenied
BULK_UPDATEalwaysrequires editable = truedenied
BULK_EXPORTalwaysrequires exportable = truerequires exportable = true

Enforcement happens in ViewPermissionService.allows() (metadata-service). The REST layer checks permissions via ViewAccessService.requireViewPermission(view, action), and the service layer (ViewDataService, BulkActionService) performs additional guard checks on the same flags.

Column-Level Permissions ​

Each column can have per-role permissions controlling visibility and editability.

Stored in ViewColumnRolePermission entity (metadata-persistence):

FieldTypeDefault
canReadbooleantrue
canWritebooleanfalse
isHiddenbooleanfalse

The ColumnPermission value object provides factory methods: fullAccess(), readOnly(), hidden(), noAccess(), readWrite().

Admins always get full column access regardless of column-level permission settings.

Authentication Filters ​

Two request filters handle authentication (before any permission check):

  1. TokenAuthorizationFilter (priority 100) — validates token type (SELECTION, TEMP_2FA, REGULAR)
  2. ActiveUserFilter (priority 200) — validates user is active with a valid session

Implementation Pattern ​

Permission checks use service injection rather than generic interceptors. Resource classes inject ViewPermissionService or WorkspacePermissionService and call permission checks explicitly:

java
// In a REST resource
viewPermissionService.requirePermission(viewUuid, userUuid, ViewAction.EDIT_ROW);

WorkspacePermissionService handles workspace-level actions (CREATE_VIEW, UPDATE_VIEW, DELETE_VIEW, CREATE_COLUMN, UPDATE_COLUMN, DELETE_COLUMN) with requirePermission() (throws ForbiddenException) and checkPermission() (returns boolean).

XSS Protection ​

Defense-in-depth approach with two implemented layers:

Input validation — @Pattern(regexp = "^(?!.*[<>]).*$") on user-generated content fields (bio, names, descriptions) in UserDTO, OrganisationDTO, JoinRequestDTO. Blocks < and > characters.

Output sanitization — sanitizeUrl() and sanitizeErrorMessage() methods in WorkspaceDatabaseConfigService to prevent injection via error messages and URLs.

Not yet implemented

Security headers (Content-Security-Policy, X-Frame-Options, X-Content-Type-Options) are not configured. No SecurityHeaderFilter exists. This is the third planned layer.

Endpoint Security Audit (2026-01-11) ​

A security audit identified 13 critical/high-priority vulnerabilities — all have been fixed and tested:

  • View endpoints allowing cross-organisation data access → fixed with requireWorkspaceMembership() checks
  • Column endpoints with no workspace membership checks → fixed with ViewAccessService.requireViewPermission()
  • Workspace list information disclosure → fixed with membership-filtered queries
  • Database connection testing without admin checks → fixed with requireOrganisationAdmin()
  • Drift detection and view column sync without permission checks → fixed

Test coverage: 35 security tests across ViewAndColumnWorkspaceIsolationTest (19 tests) and WorkspaceSecurityTest (16 tests), all passing.

Encryption ​

EncryptionUtil (common module) handles all encryption:

  • Algorithm: AES-256-GCM (12-byte random IV, 128-bit auth tag)
  • Key: From TOTP_ENCRYPTION_KEY env var (must be exactly 32 bytes). Falls back to a default key with a warning in dev.
  • Legacy: Decryption falls back to AES/ECB for backwards compatibility with older encrypted data
  • Backup codes: BCrypt with cost factor 12 (one-way hash)

Used for: TOTP secrets, database connection passwords. Note: EncryptionUtil is duplicated in 3 modules (common, processor-core, workspace-api-core) — this is a known tech debt item.

Token Lifecycle ​

TokenService manages authentication tokens with type-based expiration:

Token TypeExpiryLinked To
PASSWORD_RESET1 hourUser
EMAIL_VERIFICATION24 hoursUser
INVITATION2 daysOrganisationMembership

On generation, all existing tokens of the same type for the user are invalidated first. Tokens are validated by checking: exists, not expired, not already used. Validation marks the token as used=true.

Invitation tokens have special handling — they link to an OrganisationMembership rather than a user, and are cleaned up (membership reference nulled) when memberships are deleted.

User Session Management ​

UserSessionService tracks server-side sessions:

  • Each login creates a UserSession record (sessionId UUID, device, deviceName, ipAddress, location, userAgent, organisationSlug)
  • Only one session is marked as current per user
  • User-Agent is parsed to extract browser and OS (e.g. "Chrome on macOS")
  • Sessions can be revoked (sets revokedAt), but users cannot revoke their own current session
  • Device names can be renamed by the user

The ActiveUserFilter (priority 200) validates the session on every request.

Guest Access Tokens ​

GuestTokenService provides view-level guest access without organisation membership:

  • Token format: 32 bytes from SecureRandom, URL-safe Base64 (43 characters)
  • Storage: SHA-256 hash stored in DB (64 hex characters). Plaintext returned only once on creation.
  • Validation: checks existence, not revoked, not expired, not over usage limit (currentUses >= maxUses)
  • Roles: GUEST_VIEWER (read-only) or GUEST_EDITOR (read-write)
  • Usage tracking: recordAccess() increments currentUses and updates lastAccessedAt

Workspace API Keys ​

WorkspaceApiKeyService manages API keys for external service access (Jotform, Zapier, Make):

  • Key format: sk_ prefix + 32 bytes SecureRandom URL-safe Base64 (total: sk_ + 43 chars)
  • Storage: SHA-256 hash (64 hex characters) + first 8 chars as keyPrefix for identification
  • Permissions: READ_ONLY or READ_WRITE (enforced by ApiKeyAuthInterceptor in workspace-api)
  • Features: enabled/disabled flag, optional expiration date, lastUsedAt tracking
  • Administration: Requires WORKSPACE_ADMIN permission. Key creation returns plaintext once; revocation soft-deletes by setting enabled = false

The ApiKeyAuthInterceptor (Spring Boot) also supports public access — entities with publicAccess set to READ_ONLY or READ_WRITE are accessible without an API key.

Not Yet Implemented ​

  • Entity-level permissions for the generated workspace API (design docs exist but not coded)
  • Permission caching (@CacheResult / @CacheInvalidate pattern proposed but not active)
  • Security headers (CSP, X-Frame-Options — see XSS section above)

SchemaStack Internal Developer Documentation