Authorization and RBAC
Once a user or service has been authenticated, the MATIH Platform must determine what actions they are permitted to perform. Authorization is implemented through a layered model that combines Role-Based Access Control (RBAC), resource-level policies, attribute-based access control (ABAC), and Open Policy Agent (OPA) integration. This section covers the entire authorization stack, from the RbacService in the commons library to OPA policy evaluation at the Kubernetes level.
Authorization Architecture
Authorization decisions in MATIH flow through multiple layers:
Request with JWT Token
|
v
[1. Token Validation] -- Extract roles, tenant_id, permissions
|
v
[2. RBAC Check] -- RbacService.hasPermission(userId, permission)
|
v
[3. Resource Policy] -- PermissionEvaluator.evaluate(userId, resource, action, instance)
|
v
[4. Tenant Isolation] -- Verify resource belongs to user's tenant
|
v
[5. OPA Policy] -- External policy decision for complex rules
|
v
ALLOW / DENYEach layer can independently deny access. All layers must pass for a request to be authorized.
Role-Based Access Control (RBAC)
The RbacService class provides the core RBAC implementation. It manages roles, permissions, user-role assignments, and permission checks with an in-memory cache for performance.
Standard Platform Roles
MATIH defines five standard roles with a clear permission hierarchy:
| Role | Permissions | Description |
|---|---|---|
super_admin | * (wildcard -- all permissions) | Full platform access, system-level operations |
tenant_admin | users:*, settings:*, reports:*, audit:read | Tenant-level administration |
operator | data:*, pipelines:*, reports:read | Data operations and pipeline management |
analyst | data:read, queries:*, reports:* | Data analysis and reporting |
viewer | data:read, reports:read | Read-only access to data and reports |
These roles are registered at startup:
RbacService rbac = new RbacService();
rbac.registerStandardRoles();Permission Format
Permissions follow a resource:action convention:
users:read -- Read user records
users:write -- Create or update users
users:delete -- Delete users
queries:execute -- Execute SQL queries
pipelines:write -- Create or modify pipelines
data:read -- Read data from data sources
settings:write -- Modify tenant settings
audit:read -- View audit logs
* -- Wildcard: all permissionsWildcard matching is supported at both the resource and action levels:
| Pattern | Matches |
|---|---|
* | Everything |
users:* | All actions on users |
*:read | Read action on all resources |
data:read | Exact match: read action on data |
Role Hierarchy and Inheritance
Roles can inherit permissions from parent roles, enabling hierarchical permission structures:
// Define a base role
rbac.registerRole("data_reader", Set.of("data:read"));
// Define a role that inherits from data_reader
rbac.registerRole("data_writer",
Set.of("data:write", "data:delete"),
Set.of("data_reader") // Parent roles
);
// data_writer now has: data:read, data:write, data:deleteThe inheritance system prevents infinite loops by tracking visited roles during permission resolution:
private void collectPermissions(String roleName, Set<String> permissions, Set<String> visited) {
if (visited.contains(roleName)) {
return; // Prevent infinite loops in circular inheritance
}
visited.add(roleName);
Role role = roles.get(roleName);
if (role != null) {
permissions.addAll(role.permissions());
for (String parent : role.parentRoles()) {
collectPermissions(parent, permissions, visited);
}
}
}Permission Checks
The RbacService provides multiple check methods:
// Check a single permission
boolean canRead = rbac.hasPermission("user-123", "data:read");
// Check any of multiple permissions
boolean canAccess = rbac.hasAnyPermission("user-123", "data:read", "data:write");
// Check all of multiple permissions
boolean canManage = rbac.hasAllPermissions("user-123", "data:read", "data:write", "data:delete");
// Check role membership
boolean isAdmin = rbac.hasRole("user-123", "tenant_admin");
// Check any of multiple roles
boolean canOperate = rbac.hasAnyRole("user-123", "operator", "tenant_admin");
// Check resource-level permission (resource:action pattern)
boolean canDeleteUser = rbac.hasResourcePermission("user-123", "users", "delete");Effective Permissions
The getEffectivePermissions method computes the complete set of permissions for a user, including those inherited through role hierarchy:
Set<String> allPermissions = rbac.getEffectivePermissions("user-123");
// Returns the union of all direct and inherited permissionsResults are cached by default using a ConcurrentHashMap for thread-safe, high-performance lookups. The cache is automatically invalidated when roles or assignments change.
Permission Cache
The RBAC service includes a built-in permission cache:
// Create with caching enabled (default)
RbacService rbac = new RbacService(true);
// Invalidate all cached permissions
rbac.invalidateCache();
// Invalidate cache for a specific user
rbac.invalidateCache("user-123");The cache is automatically invalidated when:
- A role is registered, modified, or removed
- Role assignments change for a user
In a distributed deployment, the CachedPermissionChecker in the IAM service extends this with Redis-backed distributed caching.
The PermissionEvaluator Framework
While the RbacService handles role-based checks, the PermissionEvaluator extends authorization with resource-level and attribute-based policies.
Basic Permission Evaluation
PermissionEvaluator evaluator = new PermissionEvaluator(rbacService);
// Simple permission check (delegates to RbacService)
boolean allowed = evaluator.evaluate("user-123", "data:read");
// Resource-level check: RBAC + resource policy
boolean canEdit = evaluator.evaluate("user-123", "dashboards", "update");Resource-Level Policies
Resource policies add fine-grained control beyond RBAC. For example, an ownership policy ensures that only the creator of a resource can modify or delete it:
// Register an ownership policy for dashboards
evaluator.registerOwnershipPolicy("dashboards", Dashboard::getOwnerId);
// Now update/delete checks verify ownership
boolean canUpdate = evaluator.evaluate("user-123", "dashboards", "update", myDashboard);
// Returns true only if: user has dashboards:update AND user is the dashboard ownerSecurity Context Evaluation
The SecurityContext record encapsulates the complete authorization context:
SecurityContext context = SecurityContext.of("user-123", "acme-corp", Set.of("analyst"));
// Evaluate with full context (includes tenant isolation check)
boolean allowed = evaluator.evaluate(context, "dashboards", "read", dashboard);When evaluating with a SecurityContext, the evaluator automatically enforces tenant isolation. If the resource implements the TenantAware interface, the evaluator verifies that the resource's tenant ID matches the user's tenant ID:
// Automatic tenant isolation check
public <T> boolean evaluate(SecurityContext context, String resource, String action, T instance) {
// Tenant isolation: reject if resource belongs to different tenant
if (context.tenantId() != null && instance instanceof TenantAware tenantAware) {
if (!context.tenantId().equals(tenantAware.getTenantId())) {
return false; // Tenant isolation violation
}
}
return evaluate(context.userId(), resource, action, instance);
}Composite Policies
Policies can be composed using logical operators:
// All policies must pass
ResourcePolicy strictPolicy = new AllOfPolicy(
new OwnershipPolicy<>(Dashboard::getOwnerId),
new DepartmentPolicy()
);
// Any policy can pass
ResourcePolicy lenientPolicy = new AnyOfPolicy(
new OwnershipPolicy<>(Dashboard::getOwnerId),
new AdminOverridePolicy()
);
evaluator.registerPolicy("sensitive-reports", strictPolicy);Requirement Builders
For declarative permission requirements, the evaluator provides predicate builders:
// Create reusable permission predicates
Predicate<SecurityContext> canManageUsers = evaluator.requireAllPermissions(
"users:read", "users:write", "users:delete"
);
Predicate<SecurityContext> canAccessReports = evaluator.requireAnyPermission(
"reports:read", "reports:write"
);
Predicate<SecurityContext> mustBeAdmin = evaluator.requireRole("tenant_admin");
// Use in application logic
if (canManageUsers.test(currentContext)) {
// Proceed with user management
}Scope-Based Authorization
The IAM service implements scope-based authorization for API endpoints using the @RequiresScope annotation and the ScopeAuthorizationAspect:
@RequiresScope("queries:execute")
@PostMapping("/api/v1/queries")
public ResponseEntity<QueryResult> executeQuery(@RequestBody QueryRequest request) {
// Only accessible with queries:execute scope
}The aspect intercepts annotated methods and verifies that the authenticated principal's scopes include the required scope. This is particularly important for API key authentication, where scopes are explicitly defined at key creation time.
Spring Security Integration
At the controller level, the platform uses Spring Security's @PreAuthorize annotation for declarative authorization:
@PreAuthorize("hasRole('ADMIN') or hasAuthority('api_keys:read')")
@GetMapping("/tenant")
public ResponseEntity<Page<ApiKeyInfo>> listTenantApiKeys(...) {
// Accessible by admins or users with api_keys:read authority
}
@PreAuthorize("hasRole('ADMIN') or hasAuthority('api_keys:admin')")
@DeleteMapping("/user/{userId}/all")
public ResponseEntity<Map<String, Integer>> revokeAllUserKeys(...) {
// Restricted to admins or users with api_keys:admin authority
}Open Policy Agent (OPA) Integration
For complex authorization rules that go beyond RBAC -- such as time-based access restrictions, data classification policies, or regulatory compliance rules -- MATIH integrates with Open Policy Agent (OPA).
OPA Architecture
OPA runs as a sidecar container alongside each service pod. Authorization decisions follow this flow:
Service Pod
+-------------------------------------------+
| Application Container OPA Sidecar |
| | | |
| |-- Policy Query ------->| |
| | { | |
| | "input": { | |
| | "user": "...", | |
| | "action": "...", | |
| | "resource": ".." | |
| | } | |
| | } | |
| | |-- Evaluate against Rego policies
| |<-- Decision -----------| |
| | { "allow": true } | |
+-------------------------------------------+OPA Policy Example
Policies are written in Rego, OPA's policy language:
package matih.authorization
import future.keywords.if
import future.keywords.in
default allow := false
# Super admins can do anything
allow if {
input.user.roles[_] == "super_admin"
}
# Tenant admins can manage users within their tenant
allow if {
input.action == "manage_users"
input.user.roles[_] == "tenant_admin"
input.resource.tenant_id == input.user.tenant_id
}
# Data access is restricted to business hours for viewer role
allow if {
input.action == "read_data"
input.user.roles[_] == "viewer"
is_business_hours
}
is_business_hours if {
now := time.now_ns()
hour := time.clock(now)[0]
hour >= 8
hour < 18
}
# Sensitive data requires additional classification clearance
allow if {
input.action == "read_data"
input.resource.classification == "sensitive"
input.user.clearance_level >= 3
}OPA Policy Management
Policies are managed through Kubernetes ConfigMaps and distributed to OPA sidecars:
| Aspect | Implementation |
|---|---|
| Policy storage | ConfigMaps in the tenant namespace |
| Policy distribution | OPA bundle server or ConfigMap mounts |
| Policy versioning | Git-based version control |
| Policy testing | OPA's built-in test framework |
| Decision logging | OPA decision logs forwarded to the audit trail |
Authorization Decision Flow
The complete authorization decision process for a typical API request:
| Step | Layer | Check | Failure Response |
|---|---|---|---|
| 1 | SecurityFilter | Validate JWT token | 401 Unauthorized |
| 2 | SecurityFilter | Extract roles and tenant context | 401 Unauthorized |
| 3 | @PreAuthorize | Spring Security role/authority check | 403 Forbidden |
| 4 | @RequiresScope | Scope verification (API keys) | 403 Forbidden |
| 5 | PermissionEvaluator | RBAC permission check | 403 Forbidden |
| 6 | PermissionEvaluator | Resource policy evaluation | 403 Forbidden |
| 7 | PermissionEvaluator | Tenant isolation verification | 403 Forbidden |
| 8 | OPA Sidecar | Complex policy evaluation | 403 Forbidden |
Impersonation
The ImpersonationController allows authorized administrators to act on behalf of another user for debugging and support purposes:
# Start impersonation (requires super_admin or impersonate permission)
curl -X POST https://api.matih.ai/api/v1/impersonation/start \
-H "Authorization: Bearer {adminToken}" \
-H "Content-Type: application/json" \
-d '{ "targetUserId": "user-456", "reason": "Support ticket #1234" }'Impersonation is:
- Restricted to
super_adminor users with explicitimpersonation:startpermission - Fully audit-logged with the reason and duration
- Time-limited (configurable maximum duration)
- Clearly marked in the JWT claims so downstream services can distinguish impersonated requests
Access Request Workflow
The AccessRequestController manages the access request workflow, where users can request elevated permissions that require approval:
# Submit an access request
curl -X POST https://api.matih.ai/api/v1/access-requests \
-H "Authorization: Bearer {accessToken}" \
-H "Content-Type: application/json" \
-d '{
"requestedRole": "operator",
"reason": "Need pipeline access for Q1 data migration",
"duration": "P30D"
}'Access requests go through an approval workflow and are tracked in the audit log.
Best Practices
-
Use the principle of least privilege. Assign the minimum set of permissions needed for each role. Start with the
viewerrole and add permissions as needed. -
Prefer RBAC over direct permission assignment. Always assign permissions through roles rather than directly to users. This simplifies management and auditing.
-
Use resource policies for ownership. When a user should only modify their own resources, register an ownership policy rather than encoding the logic in business code.
-
Leverage the permission cache. The in-memory cache significantly reduces authorization latency. Invalidate it only when role definitions or assignments change.
-
Test OPA policies thoroughly. Use OPA's built-in testing framework to verify complex policies before deployment. Policy errors can silently block legitimate access.
-
Audit authorization failures. All denied authorization decisions should be logged for security monitoring and troubleshooting.
Next Steps
Continue to Tenant Isolation to understand how the platform enforces data boundaries between tenants at both the infrastructure and application layers.