Appearance
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
| Role | Scope |
|---|---|
| Owner | Full access to everything in the organisation. Bypasses all permission checks. |
| Admin | Manage workspaces, members, and settings. Inherits Admin on all workspaces. |
| Member | Basic membership. No inherited workspace access — must be explicitly added. |
Defined in OrganisationMembership.Role enum (organisation-persistence).
Workspace Roles
| Role | Inherited View Role | Description |
|---|---|---|
| Workspace Admin | View Admin | Full workspace management + all view access |
| Workspace Editor | View Editor | Read/write data in all views |
| Workspace Viewer | View Viewer | Read-only data in all views |
| Workspace Member | None | Workspace 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
| Role | Can Design | Can Read | Can Write |
|---|---|---|---|
| Admin | Yes | Yes | Yes |
| Editor | No | Yes | Yes |
| Viewer | No | Yes | No |
Defined in ViewMembership.Role enum (metadata-persistence).
Permission Resolution
Permissions are resolved by ViewPermissionService (metadata-service). The evaluation order:
- Organisation Owner → full access (always)
- Private view check → if view is private, only explicit ViewMembership counts
- Explicit ViewMembership → view-level role override
- Inherited from WorkspaceMembership → workspace role converted to view role
- Inherited from Organisation Admin → Admin access
- 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
Column-Level Permissions
Each column can have per-role permissions controlling visibility and editability.
Stored in ViewColumnRolePermission entity (metadata-persistence):
| Field | Type | Default |
|---|---|---|
canRead | boolean | true |
canWrite | boolean | false |
isHidden | boolean | false |
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):
- TokenAuthorizationFilter (priority 100) — validates token type (SELECTION, TEMP_2FA, REGULAR)
- 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_KEYenv 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 Type | Expiry | Linked To |
|---|---|---|
PASSWORD_RESET | 1 hour | User |
EMAIL_VERIFICATION | 24 hours | User |
INVITATION | 2 days | OrganisationMembership |
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
UserSessionrecord (sessionId UUID, device, deviceName, ipAddress, location, userAgent, organisationSlug) - Only one session is marked as
currentper 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) orGUEST_EDITOR(read-write) - Usage tracking:
recordAccess()incrementscurrentUsesand updateslastAccessedAt
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
keyPrefixfor identification - Permissions:
READ_ONLYorREAD_WRITE(enforced byApiKeyAuthInterceptorin workspace-api) - Features: enabled/disabled flag, optional expiration date,
lastUsedAttracking - Administration: Requires
WORKSPACE_ADMINpermission. Key creation returns plaintext once; revocation soft-deletes by settingenabled = 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/@CacheInvalidatepattern proposed but not active) - Security headers (CSP, X-Frame-Options — see XSS section above)