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:
| Component | Location | Responsibility |
|---|---|---|
| RbacService | commons/commons-java/.../security/authorization/RbacService.java | Role registry, permission resolution, cache management |
| PermissionEvaluator | commons/commons-java/.../security/authorization/PermissionEvaluator.java | Resource-level and attribute-based access control |
| TenantContextHolder | commons/commons-java/.../security/authorization/TenantContextHolder.java | Thread-local tenant context for multi-tenant isolation |
| AuditLogger | commons/commons-java/.../security/audit/AuditLogger.java | Authorization 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:readThe 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());
}| Role | Description | Key Permissions |
|---|---|---|
| super_admin | Platform-wide administrative access | * (all permissions) |
| tenant_admin | Tenant-level administration | User management, settings, reports, audit |
| operator | Data pipeline operations | Data read/write, pipeline management |
| analyst | Data analysis and reporting | Data read, query execution, report creation |
| viewer | Read-only access | Data 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:
- Additive. Permissions from all assigned roles are combined. There are no deny rules.
- Recursive. Parent role permissions are recursively included.
- Cycle-safe. A visited set prevents infinite recursion from circular role inheritance.
- 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:
| Event | Invalidation Scope |
|---|---|
| Role registered or removed | Full cache clear |
| User role assigned or removed | Per-user cache clear |
| Manual invalidation | Full 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
- Check RBAC permission (role-based)
- If RBAC passes, check resource-specific policy (if registered)
- 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
- Role Hierarchy -- Built-in roles, custom roles, and inheritance
- Permission Types -- Resource, action, and scope permissions
- OPA Policies -- OPA integration for policy-as-code
- Authorization -- Complete authorization architecture