Back to Blog

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.

CoomiaPublished on September 15, 202511 min read
Share this articleTwitter / X

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:

Code
+--------+   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:

Code
                    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)
RoleLevelCore PermissionsTypical User
PLATFORM_ADMINL0All permissionsPlatform administrator
SECURITY_ADMINL1Security policies, audit, classificationSecurity officer
TENANT_ADMINL1All resources within tenantTenant administrator
DATA_STEWARDL2Data governance, lineage, qualityData steward
PROJECT_ADMINL2All operations within projectProject manager
ONTOLOGY_ADMINL2Ontology managementOntology architect
DATA_ENGINEERL3Pipeline, dataset CRUDData engineer
DATA_ANALYSTL3Data query, reports, dashboardsData analyst
APP_DEVELOPERL3Application development, ActionsApplication developer
ONTOLOGY_EDITORL3ObjectType editingOntology editor
VIEWERL4Read-only accessGeneral user
ANONYMOUSL5Public resource accessUnauthenticated user

#2.2 Role Data Model

PROTOBUF
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

Java
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

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

Code
+-------------------+------------------------------------------+
| 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:

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

Code
Tenant
  |
  +-- Project
       |
       +-- Ontology
       |    |
       |    +-- ObjectType
       |         |
       |         +-- Property (Field)
       |
       +-- Dataset
       |    |
       |    +-- Column
       |
       +-- Pipeline
       |
       +-- Dashboard
       |
       +-- Action

#4.2 Resource Identifier Design

Resources use hierarchical path identifiers:

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

Code
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

PROTOBUF
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

Java
@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

Java
@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

Java
@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

Code
+-----------------------------------------------------+
|                   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

Java
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

Java
@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

Code
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

OperationCache HitCache MissP99
Permission check0.2ms3.5ms8ms
Role resolution0.1ms2.1ms5ms
Batch check (50)1.5ms25ms40ms

#9. Testing Strategy

#9.1 Unit Tests

Java
@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

Java
@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

  1. Principle of Least Privilege: Users should only have the minimum permissions needed
  2. Keep roles under 20: Too many roles leads to management chaos
  3. Avoid direct permission assignment: All permissions through roles
  4. Regular role review: Quarterly review of role assignments
  5. Use resource scopes: Permissions should be scoped to specific projects/tenants

#10.2 Common Anti-Patterns

Code
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

  1. 12 built-in roles form a clear hierarchical inheritance tree covering platform admin, security, data, and development responsibilities
  2. Permission bitmaps use 32-bit integers to encode 16 actions, enabling O(1) permission comparison
  3. 5-level resource hierarchy provides a complete resource model from tenant to field with permission inheritance and override
  4. Caffeine caching achieves sub-millisecond permission checks with 85%+ cache hit rate
  5. Multi-tenancy isolation through gRPC context propagation ensures complete tenant data isolation
  6. 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