RBAC Implementation: Modeling Roles, Permissions, and Resources
The RBAC layer in coomia-dip is the foundation of the three-layer permission model, addressing the core question of "who can do what." This article details the design and implementation of the role inheritance tree, permission bitmaps, and resource hierarchy models, along with how gRPC integrates with Spring Security for efficient role-based authorization. The system supports 12 built-in roles, 256 fine-grained permissions, 5 resource hierarchy levels, and achieves sub-millisecond permission checks through Caffeine caching.
“Series: S6 Platform Engineering · Article 2 | Level: Advanced | Reading Time: 18 min
RBAC Implementation: Modeling Roles, Permissions, and Resources
#TL;DR
The RBAC layer in coomia-dip is the foundation of the three-layer permission model, addressing the core question of "who can do what." This article details the design and implementation of the role inheritance tree, permission bitmaps, and resource hierarchy models, along with how gRPC integrates with Spring Security for efficient role-based authorization. The system supports 12 built-in roles, 256 fine-grained permissions, 5 resource hierarchy levels, and achieves sub-millisecond permission checks through Caffeine caching.
#1. RBAC Model Overview
#1.1 The Essence of RBAC
RBAC (Role-Based Access Control) works by binding permissions to roles, then assigning roles to users. This indirection mechanism greatly simplifies permission management:
+--------+ assign +--------+ bind +------------+
| User |----------->| Role |----------->| Permission |
+--------+ +--------+ +------------+
|
inherit |
v
+--------+
| Parent |
| Role |
+--------+
#1.2 Design Goals
- Hierarchical role inheritance: Child roles automatically inherit all parent permissions
- Fine-grained permission control: Support permissions down to Object Type and Field level
- Multi-tenancy isolation: Permissions isolated at project/workspace dimension
- High-performance evaluation: Sub-millisecond permission checks
- Complete audit: All role changes and permission checks are traceable
#2. Role Model Design
#2.1 Role Inheritance Tree
coomia-dip defines a 12-node role inheritance tree:
PLATFORM_ADMIN
/ \
SECURITY_ADMIN TENANT_ADMIN
/ / \
DATA_STEWARD PROJECT_ADMIN ONTOLOGY_ADMIN
/ \ / \ |
DATA_ENGINEER DATA_ANALYST APP_DEVELOPER ONTOLOGY_EDITOR
\ | /
\ | /
+--- VIEWER (base read-only role) ---+
|
ANONYMOUS (unauthenticated)
| Role | Level | Core Permissions | Typical User |
|---|---|---|---|
| PLATFORM_ADMIN | L0 | All permissions | Platform administrator |
| SECURITY_ADMIN | L1 | Security policies, audit, classification | Security officer |
| TENANT_ADMIN | L1 | All resources within tenant | Tenant administrator |
| DATA_STEWARD | L2 | Data governance, lineage, quality | Data steward |
| PROJECT_ADMIN | L2 | All operations within project | Project manager |
| ONTOLOGY_ADMIN | L2 | Ontology management | Ontology architect |
| DATA_ENGINEER | L3 | Pipeline, dataset CRUD | Data engineer |
| DATA_ANALYST | L3 | Data query, reports, dashboards | Data analyst |
| APP_DEVELOPER | L3 | Application development, Actions | Application developer |
| ONTOLOGY_EDITOR | L3 | ObjectType editing | Ontology editor |
| VIEWER | L4 | Read-only access | General user |
| ANONYMOUS | L5 | Public resource access | Unauthenticated user |
#2.2 Role Data Model
message Role {
string id = 1; // Role ID: "role:data_analyst"
string name = 2; // Display name
string description = 3; // Description
int32 level = 4; // Hierarchy level (0=highest)
string parent_role_id = 5; // Parent role ID (inheritance)
repeated string permission_ids = 6; // Directly bound permissions
bool is_system = 7; // Is system built-in role
bool is_active = 8; // Is active
string tenant_id = 9; // Owning tenant (empty=global)
google.protobuf.Timestamp created_at = 10;
google.protobuf.Timestamp updated_at = 11;
}
#2.3 Role Inheritance Resolution Algorithm
public class RoleHierarchyResolver {
private final Map<String, Role> roleRegistry;
/**
* Resolve effective permissions for a role (including inherited).
* Uses BFS to traverse the role inheritance tree upward.
*/
public Set<Permission> resolveEffectivePermissions(String roleId) {
Set<Permission> effective = new LinkedHashSet<>();
Queue<String> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(roleId);
while (!queue.isEmpty()) {
String currentId = queue.poll();
if (!visited.add(currentId)) continue;
Role role = roleRegistry.get(currentId);
if (role == null) continue;
// Add current role's direct permissions
for (String permId : role.getPermissionIdsList()) {
effective.add(permissionRegistry.get(permId));
}
// Traverse to parent role
if (role.hasParentRoleId()) {
queue.offer(role.getParentRoleId());
}
}
return Collections.unmodifiableSet(effective);
}
}
#3. Permission Model Design
#3.1 Permission Structure
Each permission is defined by three dimensions: Action + ResourceType + Constraint
Permission = Action x ResourceType x Constraint
Examples:
READ x DATASET x {project: "alpha"}
WRITE x OBJECT_TYPE x {ontology: "customer"}
ADMIN x PROJECT x {tenant: "acme"}
#3.2 Action Type Matrix
coomia-dip defines 16 standard actions in 4 categories:
+-------------------+------------------------------------------+
| Category | Actions |
+-------------------+------------------------------------------+
| Data (CRUD) | CREATE, READ, UPDATE, DELETE |
| Bulk | BATCH_READ, BATCH_WRITE, EXPORT, IMPORT |
| Admin | ADMIN, CONFIGURE, GRANT, REVOKE |
| Special | EXECUTE, SUBSCRIBE, APPROVE, SHARE |
+-------------------+------------------------------------------+
#3.3 Permission Bitmap Optimization
For efficient storage and comparison, coomia-dip uses bitmap encoding for permissions:
public class PermissionBitmap {
// 16 actions -> 16 bits
private static final int CREATE = 1 << 0; // 0x0001
private static final int READ = 1 << 1; // 0x0002
private static final int UPDATE = 1 << 2; // 0x0004
private static final int DELETE = 1 << 3; // 0x0008
private static final int EXPORT = 1 << 4; // 0x0010
private static final int IMPORT = 1 << 5; // 0x0020
private static final int ADMIN = 1 << 6; // 0x0040
private static final int EXECUTE = 1 << 7; // 0x0080
private int bitmap;
public boolean hasPermission(int action) {
return (bitmap & action) == action;
}
public void grant(int action) {
bitmap |= action;
}
public void revoke(int action) {
bitmap &= ~action;
}
public boolean hasAllPermissions(int... actions) {
int combined = 0;
for (int a : actions) combined |= a;
return (bitmap & combined) == combined;
}
public boolean hasAnyPermission(int... actions) {
int combined = 0;
for (int a : actions) combined |= a;
return (bitmap & combined) != 0;
}
}
Bitmap advantages:
- Space efficiency: A single int (32 bits) stores all action permissions
- Comparison efficiency: Bitwise operations in O(1) time
- Merge efficiency: Permission merge during role inheritance needs only OR operation
#4. Resource Hierarchy Model
#4.1 Five-Level Resource Hierarchy
coomia-dip organizes resources into a 5-level hierarchy:
Tenant
|
+-- Project
|
+-- Ontology
| |
| +-- ObjectType
| |
| +-- Property (Field)
|
+-- Dataset
| |
| +-- Column
|
+-- Pipeline
|
+-- Dashboard
|
+-- Action
#4.2 Resource Identifier Design
Resources use hierarchical path identifiers:
Format: /{tenant}/{project}/{resource_type}/{resource_id}[/{sub}]
Examples:
/acme/alpha/ontology/customer
/acme/alpha/ontology/customer/property/phone
/acme/alpha/dataset/sales_2024
/acme/alpha/dashboard/kpi_board
#4.3 Permission Inheritance Rules
In the resource hierarchy, permissions inherit top-down:
Rules:
1. Tenant admin -> auto owns all projects' admin perms
2. Project admin -> auto owns all resources' admin perms
3. Ontology admin -> auto owns all ObjectTypes' admin perms
4. ObjectType READ -> auto includes all Properties' READ
5. Explicit sub-resource perms can override inherited perms
Inheritance chain:
Tenant ADMIN -> Project ADMIN -> Ontology ADMIN
-> ObjectType ADMIN -> Property ADMIN
#5. gRPC Service Implementation
#5.1 RoleManagementService
service RoleManagementService {
rpc CreateRole(CreateRoleRequest) returns (Role);
rpc GetRole(GetRoleRequest) returns (Role);
rpc UpdateRole(UpdateRoleRequest) returns (Role);
rpc DeleteRole(DeleteRoleRequest) returns (google.protobuf.Empty);
rpc ListRoles(ListRolesRequest) returns (ListRolesResponse);
rpc AssignRole(AssignRoleRequest) returns (RoleBinding);
rpc RevokeRole(RevokeRoleRequest) returns (google.protobuf.Empty);
rpc GetUserRoles(GetUserRolesRequest) returns (UserRolesResponse);
rpc GetEffectivePermissions(GetEffectivePermissionsRequest)
returns (EffectivePermissionsResponse);
rpc CheckPermission(CheckPermissionRequest)
returns (CheckPermissionResponse);
}
#5.2 Permission Check Implementation
@GrpcService
public class RoleManagementServiceImpl
extends RoleManagementServiceGrpc
.RoleManagementServiceImplBase {
private final RoleRepository roleRepository;
private final RoleBindingRepository bindingRepository;
private final RoleHierarchyResolver hierarchyResolver;
private final Cache<String, Set<Permission>> permissionCache;
@Override
public void checkPermission(
CheckPermissionRequest request,
StreamObserver<CheckPermissionResponse> observer) {
String subjectId = request.getSubjectId();
String action = request.getAction();
String resourcePath = request.getResourcePath();
// 1. Get effective permissions from cache
Set<Permission> effective = permissionCache.get(
subjectId, () -> {
List<RoleBinding> bindings = bindingRepository
.findBySubjectId(subjectId);
Set<Permission> perms = new LinkedHashSet<>();
for (RoleBinding b : bindings) {
perms.addAll(hierarchyResolver
.resolveEffectivePermissions(
b.getRoleId()));
}
return perms;
});
// 2. Match permissions
boolean allowed = effective.stream()
.anyMatch(p -> p.matchesAction(action)
&& p.matchesResource(resourcePath));
// 3. Return result
observer.onNext(CheckPermissionResponse.newBuilder()
.setAllowed(allowed)
.setReason(allowed
? "Permission granted via RBAC"
: "No matching permission found")
.build());
observer.onCompleted();
}
}
#6. Spring Security Integration
#6.1 Custom AuthenticationProvider
@Component
public class GrpcRbacAuthenticationProvider
implements AuthenticationProvider {
private final RoleManagementServiceBlockingStub roleService;
@Override
public Authentication authenticate(Authentication auth)
throws AuthenticationException {
String credentials = auth.getCredentials().toString();
Claims claims = jwtParser.parseClaimsJws(credentials)
.getBody();
UserRolesResponse rolesResp = roleService.getUserRoles(
GetUserRolesRequest.newBuilder()
.setUserId(claims.getSubject())
.build()
);
List<GrantedAuthority> authorities = rolesResp
.getRolesList().stream()
.map(r -> new SimpleGrantedAuthority(
"ROLE_" + r.getName()))
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(
claims.getSubject(), null, authorities);
}
}
#6.2 Method-Level Permission Annotations
@RestController
@RequestMapping("/api/v1/ontology")
public class OntologyController {
@PreAuthorize("hasRole('ONTOLOGY_ADMIN') "
+ "or hasRole('PLATFORM_ADMIN')")
@PostMapping
public ResponseEntity<Ontology> createOntology(
@RequestBody CreateOntologyRequest request) {
return ResponseEntity.ok(
ontologyService.create(request));
}
@PreAuthorize("hasRole('VIEWER')")
@GetMapping("/{id}")
public ResponseEntity<Ontology> getOntology(
@PathVariable String id) {
return ResponseEntity.ok(
ontologyService.getById(id));
}
@PreAuthorize("@rbacChecker.hasResourcePermission("
+ "#id, 'UPDATE')")
@PutMapping("/{id}")
public ResponseEntity<Ontology> updateOntology(
@PathVariable String id,
@RequestBody UpdateOntologyRequest request) {
return ResponseEntity.ok(
ontologyService.update(id, request));
}
}
#7. Multi-Tenancy Isolation
#7.1 Tenant Isolation Architecture
+-----------------------------------------------------+
| PLATFORM_ADMIN |
| (Cross-tenant, global mgmt) |
+-----------------------------------------------------+
| | |
+--------v--------+ +-------v--------+ +------v-------+
| Tenant: ACME | | Tenant: BETA | | Tenant: GAMMA|
| | | | | |
| TENANT_ADMIN | | TENANT_ADMIN | | TENANT_ADMIN |
| PROJECT_ADMIN | | PROJECT_ADMIN | | DATA_ANALYST|
| DATA_ANALYST | | VIEWER | | VIEWER |
+-----------------+ +----------------+ +--------------+
Isolation rules:
- ACME users can NEVER access BETA resources
- Roles are completely independent between tenants
- PLATFORM_ADMIN can operate across tenants
#7.2 Tenant Context Propagation
public class TenantContextInterceptor
implements ServerInterceptor {
@Override
public <ReqT, RespT> ServerCall.Listener<ReqT>
interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
String tenantId = headers.get(
Metadata.Key.of("x-tenant-id",
Metadata.ASCII_STRING_MARSHALLER));
Context ctx = Context.current()
.withValue(TENANT_ID_KEY, tenantId);
return Contexts.interceptCall(
ctx, call, headers, next);
}
}
#8. Caching Strategy and Performance
#8.1 Caffeine Local Cache Configuration
@Configuration
public class RbacCacheConfig {
@Bean
public Cache<String, Set<Permission>> permissionCache() {
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofSeconds(30))
.recordStats()
.build();
}
@Bean
public Cache<String, RoleHierarchy> hierarchyCache() {
return Caffeine.newBuilder()
.maximumSize(100)
.expireAfterWrite(Duration.ofMinutes(5))
.build();
}
}
#8.2 Cache Invalidation Strategy
Event -> Invalidation Scope
-------------------------------------------------
Role permission change -> All users with that role
Role hierarchy change -> All hierarchy cache + affected users
User role binding change -> That user's permission cache
Resource hierarchy change -> All permission caches (full refresh)
#8.3 Performance Benchmarks
| Operation | Cache Hit | Cache Miss | P99 |
|---|---|---|---|
| Permission check | 0.2ms | 3.5ms | 8ms |
| Role resolution | 0.1ms | 2.1ms | 5ms |
| Batch check (50) | 1.5ms | 25ms | 40ms |
#9. Testing Strategy
#9.1 Unit Tests
@ExtendWith(MockitoExtension.class)
class RoleHierarchyResolverTest {
@Mock private RoleRepository roleRepository;
@InjectMocks private RoleHierarchyResolver resolver;
@Test
void shouldInheritParentPermissions() {
Role viewer = createRole("VIEWER", null,
List.of("perm:read"));
Role analyst = createRole("DATA_ANALYST", "VIEWER",
List.of("perm:export", "perm:query"));
when(roleRepository.findById("DATA_ANALYST"))
.thenReturn(Optional.of(analyst));
when(roleRepository.findById("VIEWER"))
.thenReturn(Optional.of(viewer));
Set<Permission> effective = resolver
.resolveEffectivePermissions("DATA_ANALYST");
assertThat(effective).hasSize(3);
assertThat(effective).extracting(Permission::getId)
.containsExactlyInAnyOrder(
"perm:read", "perm:export", "perm:query");
}
@Test
void shouldHandleCircularInheritance() {
Role a = createRole("A", "B", List.of("perm:a"));
Role b = createRole("B", "A", List.of("perm:b"));
when(roleRepository.findById("A"))
.thenReturn(Optional.of(a));
when(roleRepository.findById("B"))
.thenReturn(Optional.of(b));
Set<Permission> effective = resolver
.resolveEffectivePermissions("A");
assertThat(effective).hasSize(2);
// BFS visited set prevents infinite loops
}
}
#9.2 Integration Tests
@SpringBootTest
@Testcontainers
class RbacIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:15");
@Autowired
private RoleManagementServiceBlockingStub roleService;
@Test
void fullRbacWorkflow() {
// 1. Create custom role
Role customRole = roleService.createRole(
CreateRoleRequest.newBuilder()
.setName("CUSTOM_ANALYST")
.setParentRoleId("VIEWER")
.addPermissionIds("perm:query")
.addPermissionIds("perm:export")
.build()
);
// 2. Assign role to user
roleService.assignRole(
AssignRoleRequest.newBuilder()
.setSubjectId("user:alice")
.setRoleId(customRole.getId())
.setResourceScope("/acme/project-alpha")
.build()
);
// 3. Check permission - should allow
var response = roleService.checkPermission(
CheckPermissionRequest.newBuilder()
.setSubjectId("user:alice")
.setAction("READ")
.setResourcePath(
"/acme/project-alpha/dataset/sales")
.build()
);
assertThat(response.getAllowed()).isTrue();
// 4. Cross-project - should deny
var crossProject = roleService.checkPermission(
CheckPermissionRequest.newBuilder()
.setSubjectId("user:alice")
.setAction("READ")
.setResourcePath(
"/acme/project-beta/dataset/hr")
.build()
);
assertThat(crossProject.getAllowed()).isFalse();
}
}
#10. Best Practices
#10.1 Role Design Principles
- Principle of Least Privilege: Users should only have the minimum permissions needed
- Keep roles under 20: Too many roles leads to management chaos
- Avoid direct permission assignment: All permissions through roles
- Regular role review: Quarterly review of role assignments
- Use resource scopes: Permissions should be scoped to specific projects/tenants
#10.2 Common Anti-Patterns
Anti-Pattern 1: God Role
BAD: Create a "SUPER_USER" role with all permissions
GOOD: Use PLATFORM_ADMIN with limited assignment
Anti-Pattern 2: Permission Creep
BAD: Users retain permissions after leaving projects
GOOD: Implement automatic permission revocation
Anti-Pattern 3: Hardcoded Role Checks
BAD: if (user.role == "admin") { ... }
GOOD: if (rbacChecker.hasPermission(user, "CREATE", res))
Anti-Pattern 4: Shared Accounts
BAD: Multiple people using one service account
GOOD: Each service uses an independent ServiceAccount
#Key Takeaways
- 12 built-in roles form a clear hierarchical inheritance tree covering platform admin, security, data, and development responsibilities
- Permission bitmaps use 32-bit integers to encode 16 actions, enabling O(1) permission comparison
- 5-level resource hierarchy provides a complete resource model from tenant to field with permission inheritance and override
- Caffeine caching achieves sub-millisecond permission checks with 85%+ cache hit rate
- Multi-tenancy isolation through gRPC context propagation ensures complete tenant data isolation
- Complete test coverage including unit tests, integration tests, and edge cases like circular inheritance
#Next Article
The next article S6-03: ABAC Implementation: Attribute-Based Fine-Grained Access Control will detail how the ABAC layer uses subject, resource, and environment attribute combinations to achieve conditional fine-grained access control.
Tags: #RBAC #RoleManagement #PermissionModel #SpringSecurity #gRPC #Caffeine #Caching #MultiTenancy #coomia-dip #PlatformEngineering