MATIH Platform is in active MVP development. Documentation reflects current implementation status.
3. Security & Multi-Tenancy
Authorization & RBAC

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 / DENY

Each 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:

RolePermissionsDescription
super_admin* (wildcard -- all permissions)Full platform access, system-level operations
tenant_adminusers:*, settings:*, reports:*, audit:readTenant-level administration
operatordata:*, pipelines:*, reports:readData operations and pipeline management
analystdata:read, queries:*, reports:*Data analysis and reporting
viewerdata:read, reports:readRead-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 permissions

Wildcard matching is supported at both the resource and action levels:

PatternMatches
*Everything
users:*All actions on users
*:readRead action on all resources
data:readExact 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:delete

The 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 permissions

Results 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 owner

Security 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:

AspectImplementation
Policy storageConfigMaps in the tenant namespace
Policy distributionOPA bundle server or ConfigMap mounts
Policy versioningGit-based version control
Policy testingOPA's built-in test framework
Decision loggingOPA decision logs forwarded to the audit trail

Authorization Decision Flow

The complete authorization decision process for a typical API request:

StepLayerCheckFailure Response
1SecurityFilterValidate JWT token401 Unauthorized
2SecurityFilterExtract roles and tenant context401 Unauthorized
3@PreAuthorizeSpring Security role/authority check403 Forbidden
4@RequiresScopeScope verification (API keys)403 Forbidden
5PermissionEvaluatorRBAC permission check403 Forbidden
6PermissionEvaluatorResource policy evaluation403 Forbidden
7PermissionEvaluatorTenant isolation verification403 Forbidden
8OPA SidecarComplex policy evaluation403 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_admin or users with explicit impersonation:start permission
  • 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

  1. Use the principle of least privilege. Assign the minimum set of permissions needed for each role. Start with the viewer role and add permissions as needed.

  2. Prefer RBAC over direct permission assignment. Always assign permissions through roles rather than directly to users. This simplifies management and auditing.

  3. 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.

  4. Leverage the permission cache. The in-memory cache significantly reduces authorization latency. Invalidate it only when role definitions or assignments change.

  5. Test OPA policies thoroughly. Use OPA's built-in testing framework to verify complex policies before deployment. Policy errors can silently block legitimate access.

  6. 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.