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

RBAC Model

Role-Based Access Control (RBAC) is the primary authorization mechanism in the MATIH Platform. Every user action -- from querying data to managing tenants -- is governed by a permission check that evaluates the user's assigned roles against the required permissions for the operation. This page provides a comprehensive overview of the RBAC model, its implementation, and how it integrates with the broader security architecture.


Architecture

The RBAC system is implemented in the commons library and used by every service in the platform:

ComponentLocationResponsibility
RbacServicecommons/commons-java/.../security/authorization/RbacService.javaRole registry, permission resolution, cache management
PermissionEvaluatorcommons/commons-java/.../security/authorization/PermissionEvaluator.javaResource-level and attribute-based access control
TenantContextHoldercommons/commons-java/.../security/authorization/TenantContextHolder.javaThread-local tenant context for multi-tenant isolation
AuditLoggercommons/commons-java/.../security/audit/AuditLogger.javaAuthorization event logging

Core Concepts

Roles

A role is a named collection of permissions. Roles can inherit from parent roles, forming a hierarchy. The Role record is defined as:

// From RbacService.java
public record Role(
    String name,
    Set<String> permissions,
    Set<String> parentRoles
) {
    public Role {
        permissions = permissions != null ? Set.copyOf(permissions) : Set.of();
        parentRoles = parentRoles != null ? Set.copyOf(parentRoles) : Set.of();
    }
}

Roles are immutable once created. Their permissions and parent roles are stored as unmodifiable sets.

Permissions

Permissions are string-based identifiers following a resource:action convention:

data:read
queries:execute
pipelines:write
reports:delete
settings:write
audit:read

The wildcard permission * grants access to everything. Resource-level wildcards like data:* or *:read are also supported.

User-Role Assignments

Users are assigned roles within a tenant context. A user can have different roles in different tenants. Role assignments are stored in the IAM service database and cached in the RbacService.


Standard Roles

The platform ships with five built-in roles, registered via the registerStandardRoles() method:

// From RbacService.java
public void registerStandardRoles() {
    registerRole("super_admin", Set.of("*"), Set.of());
 
    registerRole("tenant_admin", Set.of(
        "users:read", "users:write", "users:delete",
        "settings:read", "settings:write",
        "reports:read", "reports:write",
        "audit:read"
    ), Set.of());
 
    registerRole("operator", Set.of(
        "data:read", "data:write",
        "pipelines:read", "pipelines:write", "pipelines:execute",
        "reports:read"
    ), Set.of());
 
    registerRole("analyst", Set.of(
        "data:read",
        "queries:read", "queries:write", "queries:execute",
        "reports:read", "reports:write"
    ), Set.of());
 
    registerRole("viewer", Set.of(
        "data:read",
        "reports:read"
    ), Set.of());
}
RoleDescriptionKey Permissions
super_adminPlatform-wide administrative access* (all permissions)
tenant_adminTenant-level administrationUser management, settings, reports, audit
operatorData pipeline operationsData read/write, pipeline management
analystData analysis and reportingData read, query execution, report creation
viewerRead-only accessData read, report read

Permission Resolution

When checking whether a user has a specific permission, the RbacService resolves the user's effective permissions by collecting permissions from all assigned roles, including inherited permissions from parent roles.

Effective Permission Computation

// From RbacService.java
public Set<String> getEffectivePermissions(String userId) {
    if (cacheEnabled) {
        return permissionCache.computeIfAbsent(userId, this::computeEffectivePermissions);
    }
    return computeEffectivePermissions(userId);
}
 
private Set<String> computeEffectivePermissions(String userId) {
    Set<String> assignedRoles = getUserRoles(userId);
    Set<String> permissions = new HashSet<>();
 
    for (String roleName : assignedRoles) {
        collectPermissions(roleName, permissions, new HashSet<>());
    }
 
    return Collections.unmodifiableSet(permissions);
}
 
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);
        }
    }
}

Key aspects of the resolution algorithm:

  1. Additive. Permissions from all assigned roles are combined. There are no deny rules.
  2. Recursive. Parent role permissions are recursively included.
  3. Cycle-safe. A visited set prevents infinite recursion from circular role inheritance.
  4. Cached. Computed permissions are cached per user for performance.

Permission Check Methods

The RbacService provides several check methods:

// Single permission check
public boolean hasPermission(String userId, String permission) {
    return getEffectivePermissions(userId).contains(permission) ||
           getEffectivePermissions(userId).contains("*");
}
 
// Any of multiple permissions
public boolean hasAnyPermission(String userId, String... permissions) {
    Set<String> effective = getEffectivePermissions(userId);
    if (effective.contains("*")) return true;
    for (String permission : permissions) {
        if (effective.contains(permission)) return true;
    }
    return false;
}
 
// All of multiple permissions
public boolean hasAllPermissions(String userId, String... permissions) {
    Set<String> effective = getEffectivePermissions(userId);
    if (effective.contains("*")) return true;
    for (String permission : permissions) {
        if (!effective.contains(permission)) return false;
    }
    return true;
}
 
// Resource-level permission with wildcards
public boolean hasResourcePermission(String userId, String resource, String action) {
    String permission = resource + ":" + action;
    String wildcardResource = resource + ":*";
    String wildcardAction = "*:" + action;
    return hasAnyPermission(userId, permission, wildcardResource, wildcardAction);
}

Permission Caching

The RBAC service uses a ConcurrentHashMap-based cache to avoid recomputing effective permissions on every request:

private final Map<String, Set<String>> permissionCache = new ConcurrentHashMap<>();

Cache invalidation occurs when:

EventInvalidation Scope
Role registered or removedFull cache clear
User role assigned or removedPer-user cache clear
Manual invalidationFull or per-user
public void invalidateCache() {
    permissionCache.clear();
}
 
public void invalidateCache(String userId) {
    permissionCache.remove(userId);
}

PermissionEvaluator

The PermissionEvaluator extends the basic RBAC model with resource-level and attribute-based access control. It wraps the RbacService and adds policies that can inspect the actual resource being accessed.

// From PermissionEvaluator.java
public class PermissionEvaluator {
 
    private final RbacService rbacService;
    private final Map<String, ResourcePolicy> resourcePolicies = new HashMap<>();
 
    public PermissionEvaluator(RbacService rbacService) {
        this.rbacService = rbacService;
    }
}

Evaluation Flow

  1. Check RBAC permission (role-based)
  2. If RBAC passes, check resource-specific policy (if registered)
  3. If evaluating with a SecurityContext, also check tenant isolation
public <T> boolean evaluate(SecurityContext context, String resource,
                            String action, T resourceInstance) {
    if (context == null || context.userId() == null) {
        return false;
    }
 
    // Tenant isolation check
    if (context.tenantId() != null && resourceInstance instanceof TenantAware tenantAware) {
        if (!context.tenantId().equals(tenantAware.getTenantId())) {
            return false; // Tenant isolation violation
        }
    }
 
    return evaluate(context.userId(), resource, action, resourceInstance);
}

Requirement Builders

The evaluator provides a fluent API for creating permission requirements:

// Single permission requirement
Predicate<SecurityContext> canReadData = evaluator.requirePermission("data:read");
 
// Any of several permissions
Predicate<SecurityContext> canManagePipelines = evaluator.requireAnyPermission(
    "pipelines:read", "pipelines:write", "pipelines:execute"
);
 
// Role requirement
Predicate<SecurityContext> isAdmin = evaluator.requireRole("tenant_admin");

Audit Integration

Every authorization decision is logged for compliance and security monitoring:

// From AuditLogger.java
public void logAuthorization(String userId, String resource, String action, boolean granted) {
    AuditEvent.Builder builder = AuditEvent.authorization()
        .action(granted ? AuditEvent.Action.PERMISSION_GRANTED
                       : AuditEvent.Action.PERMISSION_DENIED)
        .userId(userId)
        .resourceType(resource)
        .detail("requestedAction", action)
        .tenantId(LogContext.getTenantId())
        .correlationId(LogContext.getCorrelationId());
 
    if (granted) {
        builder.success();
    } else {
        builder.failure("Permission denied");
    }
 
    log(builder.build());
}

Related Pages