Data Isolation
Data Isolation
Section titled “Data Isolation”Mantis enforces strict data isolation between tenants at every level of the application.
Overview
Section titled “Overview”Tenant isolation is enforced through multiple layers:
Authentication-Based Isolation
Section titled “Authentication-Based Isolation”JWT Claims Structure
Section titled “JWT Claims Structure”User tokens contain tenant scope information:
{ "sub": "019b937d-4862-8e07-ac3a-1b8589d1b807", "username": "alice", "email": "alice@acme.com", "tenant_id": "019b937d-4862-84ae-9c2a-6ac230cc4c7e", "roles": ["operator"], "exp": 1704931200, "iat": 1704844800}| Claim | Isolation Impact |
|---|---|
sub | User ID — audit trail attribution (parsed server-side) |
tenant_id | Scopes all queries to this tenant |
roles | Determines permitted operations |
User Types
Section titled “User Types”| User Type | tenant_id | Access Scope |
|---|---|---|
| Platform Admin | null | All resources across all tenants |
| Tenant User | <tenant_id> | Only resources for their tenant |
API-Level Enforcement
Section titled “API-Level Enforcement”Request Flow
Section titled “Request Flow”Access Verification Function
Section titled “Access Verification Function”// From mandible/src/routes/tenants/helpers.rspub fn verify_tenant_access(claims: &JwtClaims, requested_tenant_id: uuid::Uuid) -> Result<(), ApiError> { // Admins (no tenant_id AND the admin role) can access all tenants if claims.tenant_id.is_none() && claims.is_admin() { return Ok(()); }
// Tenant users can only access their own tenant if let Some(user_tenant_id) = claims.tenant_id { if user_tenant_id == requested_tenant_id { return Ok(()); } }
// Return NotFound to prevent information leakage Err(ApiError::NotFound("Tenant not found".to_string()))}Security Response Pattern
Section titled “Security Response Pattern”Cross-tenant access attempts return 404 Not Found rather than 403 Forbidden:
This prevents tenant enumeration attacks by not revealing that a resource exists in another tenant.
Database-Level Filtering
Section titled “Database-Level Filtering”Automatic Query Filtering
Section titled “Automatic Query Filtering”All tenant-aware queries include automatic filtering:
// From mandible/src/routes/deployments/handlers.rs:182-184let tenant_scope = crate::routes::tenants::helpers::resolve_tenant_scope(&auth.claims)?;if let Some(tenant_id) = tenant_scope { query.tenant_id = Some(tenant_id.to_string());}Tenant-Scoped Tables
Section titled “Tenant-Scoped Tables”These tables are filtered by tenant_id:
| Table | Filter Column | Cascade Delete |
|---|---|---|
deployment_history | tenant_id | Yes |
tenants_solutions_environments | tenant_id | Yes |
tenants_targets | tenant_id | Yes |
tenant_variables | tenant_id | Yes |
tenant_common_variables | tenant_id | Yes |
audit_log_entries | tenant_id | No |
users | tenant_id | No (nullable) |
Query Examples
Section titled “Query Examples”Listing Deployments (tenant-scoped):
-- Platform admin (tenant_id = null): sees allSELECT * FROM deployment_history ORDER BY created_at DESC;
-- Tenant user (tenant_id = 1): filtered automaticallySELECT * FROM deployment_historyWHERE tenant_id = 1ORDER BY created_at DESC;Audit Log Filtering:
// Filter audit logs by tenant for tenant-scoped users// mandible/src/routes/audit/handlers.rs:171-172let user_tenant_id = auth.claims.tenant_id;if let Some(tid) = user_tenant_id { query = query.filter(tenant_id.eq(Some(tid)));}Resource Isolation Boundaries
Section titled “Resource Isolation Boundaries”Optionally Tenant-Scoped Resources
Section titled “Optionally Tenant-Scoped Resources”Actions, Sequences, Solutions, and Environments each carry a nullable tenant_id:
when it is NULL the resource is global (visible to all tenants, with entitlements);
when it is set the resource is tenant-private (visible only within that tenant). They
are therefore not platform-only/shared — they are optionally tenant-scoped.
Tenant Resources (Isolated)
Section titled “Tenant Resources (Isolated)”These resources are strictly isolated per tenant:
| Resource | Isolation | Cross-Tenant Access |
|---|---|---|
| Variables | Complete | Never visible |
| Targets | Complete | Never visible |
| Deployments | Complete | Never visible |
| Users | Complete | Never visible |
| Audit Logs | Complete | Platform admin only |
Entitlement-Based Access
Section titled “Entitlement-Based Access”Access to platform resources is granted through entitlements:
Real-Time Event Isolation
Section titled “Real-Time Event Isolation”SSE Event Filtering
Section titled “SSE Event Filtering”Server-Sent Events are filtered by tenant:
// From mandible/src/routes/sse/handlers.rs:236let tenant_id = sse_auth.claims.tenant_id;
// Events filtered before broadcastloop { match rx.recv().await { Ok(event) => { // Only emit events for user's tenant if let Some(tid) = tenant_id { if !event.matches_tenant(tid) { continue; } } // ... yield event } }}Event Visibility
Section titled “Event Visibility”| Event Type | Tenant User Visibility | Platform Admin Visibility |
|---|---|---|
| Deployment progress | Own tenant only | All tenants |
| Target status | Own tenant only | All tenants |
| Audit events | Own tenant only | All tenants |
Variable Isolation
Section titled “Variable Isolation”Encrypted Variables
Section titled “Encrypted Variables”Tenant variables are encrypted with variable-specific AAD (Additional Authenticated Data) that binds the ciphertext to the variable’s identity:
// instinct::crypto::encryption::build_tenant_var_aad// Produces: "var:{variable_id}:value"let aad = build_tenant_var_aad(variable_id);let encrypted = encryptor.encrypt_string(value, &aad)?;This ensures:
- Variables cannot be decrypted without the specific variable ID binding
- Key compromise doesn’t expose all tenant data
Variable Resolution
Section titled “Variable Resolution”Variable precedence respects tenant boundaries:
Audit Isolation
Section titled “Audit Isolation”Audit Log Filtering
Section titled “Audit Log Filtering”Tenant users only see their own audit events:
-- Automatic filter applied for tenant usersSELECT * FROM audit_log_entriesWHERE tenant_id = $user_tenant_idORDER BY created_at DESC;Audit Trail Per Tenant
Section titled “Audit Trail Per Tenant”| Event | Captured | Tenant Visible |
|---|---|---|
| Deployment started | Yes | Own tenant only |
| Variable changed | Yes | Own tenant only |
| User login | Yes | Own tenant only |
| Cross-tenant access attempt | Yes | Platform admin only |
SSO Isolation
Section titled “SSO Isolation”Tenant-Specific IdPs
Section titled “Tenant-Specific IdPs”Identity providers can be tenant-scoped:
-- Identity provider with tenant scopeCREATE TABLE identity_providers ( id UUID PRIMARY KEY, tenant_id UUID REFERENCES tenants(id), -- ... other fields);| Configuration | Behavior |
|---|---|
tenant_id = NULL | Platform-wide IdP |
tenant_id = 1 | Only Tenant 1 users can use |
User Provisioning
Section titled “User Provisioning”Users created via SSO inherit the IdP’s tenant scope:
Security Best Practices
Section titled “Security Best Practices”Defense in Depth
Section titled “Defense in Depth”Layer 1: Authentication └── JWT validation └── Token expiration
Layer 2: Authorization └── Permission checks └── Tenant access verification
Layer 3: Data Access └── Query filtering └── Row-level security
Layer 4: Audit └── Event logging └── Access trackingRecommendations
Section titled “Recommendations”- Never trust client-provided tenant_id - Always use JWT claims
- Return 404 for cross-tenant access - Prevent enumeration
- Audit all access attempts - Track suspicious patterns
- Encrypt sensitive variables - Use tenant-specific AAD
- Separate platform admins - Minimize cross-tenant access
Common Attack Vectors
Section titled “Common Attack Vectors”| Vector | Mitigation |
|---|---|
| JWT tampering | Signature verification |
| Tenant ID injection | Server-side extraction only |
| IDOR (Insecure Direct Object Reference) | Tenant filtering at query level |
| Privilege escalation | Role-based permission checks |
| Audit log tampering | Append-only audit storage |
Verification Checklist
Section titled “Verification Checklist”For Platform Administrators
Section titled “For Platform Administrators”- All API endpoints enforce tenant filtering
- No sensitive data leaks in error messages
- Audit logs capture all access attempts
- SSE events filtered by tenant
- Variable encryption uses tenant-specific AAD
For Security Audits
Section titled “For Security Audits”- Cross-tenant access returns 404 (not 403)
- No tenant_id in URL paths that could be tampered
- All queries include tenant_id WHERE clause
- Platform admin actions are logged separately
- Token refresh respects original tenant scope
Troubleshooting
Section titled “Troubleshooting”User Seeing Wrong Tenant Data
Section titled “User Seeing Wrong Tenant Data”-
Check JWT claims:
Terminal window # Decode JWT payloadecho $TOKEN | cut -d'.' -f2 | base64 -d | jq '.tenant_id' -
Verify user assignment:
SELECT tenant_id FROM users WHERE id = $user_id;
Audit Logs Missing Events
Section titled “Audit Logs Missing Events”- Check tenant filter in query
- Verify audit logging is enabled
- Check if event is platform-level (no tenant_id)
Next Steps
Section titled “Next Steps”- Tenant Administration - Manage tenants day-to-day
- Audit Logs - Audit configuration
- Users - User management
