| Author | Sanghun Lee (sanghun@lablup.com) |
|---|---|
| Status | Draft |
| Created | 2026-02-10 |
| Created-Version | 26.2.0 |
| Target-Version | |
| Implemented-Version |
- JIRA: BA-4179 — Implement RBAC Entity Relationship Model
- JIRA: BA-4218 — BEP-1048 document registration
- GitHub: #8531
- Related BEPs:
- BEP-1008: RBAC - Original RBAC technical design
- BEP-1012: RBAC (detailed) - RBAC feature specification
BEP-1008 and BEP-1012 define RBAC roles, permissions, and scope hierarchy, but do not specify how entity visibility integrates with RBAC. Currently:
- Each entity type implements its own visibility logic, determining which instances a user can see.
- No unified model exists for how entity relationships affect traversal and access.
- Association tables (
AssociationContainerRegistriesGroupsRow,ScalingGroupForDomainRow, etc.) are scattered across the codebase with inconsistent patterns.
This proposal introduces an Entity Relationship Model that classifies all entity relationships into a small set of types, unifying visibility logic into a single table (association_scopes_entities).
Entity visibility is currently handled per-entity:
- ResourceGroup: Uses separate junction tables (
sgroups_for_domains,sgroups_for_groups,sgroups_for_keypairs) to determine which resource groups are visible to which scopes. - ContainerRegistry: Uses
association_container_registries_groupsjunction table for project-level visibility. - VFolder: Uses
vfolder_permissionsandvfolder_invitationsfor sharing. - Session/Kernel/Agent: Visibility determined by ad-hoc query joins.
- Inconsistent patterns: Each entity type uses a different mechanism for scope-based visibility.
- No traversal model: No unified way to determine what entities are reachable from a given entry point.
- DB table proliferation: Each N:N scope mapping requires its own junction table.
- RBAC gap: BEP-1008/1012 define what operations a user can perform, but not which entities are visible in the first place.
All entity relationships are classified into exactly three types:
| Type | Semantics | Storage | GQL Sub-field | Permission |
|---|---|---|---|---|
guarded |
Independent entity; no edge relationship | N/A (no edge) | No | Separate RBAC check required |
auto |
Composition; permission delegation from parent | DB (association_scopes_entities, relation_type=auto) |
Yes | Role-defined permissions flow through |
ref |
Read-only reference; no permission delegation | DB (association_scopes_entities, relation_type=ref) |
Yes (read-only) | Parent's CRUD → child's READ only |
Key semantics:
- Guarded is not an edge — it represents the absence of a relationship. A's permissions are independent of B's. B requires its own Root Query with its own RBAC check. No GQL sub-field exists between them.
- Auto edges are stored per-instance in
association_scopes_entitieswithrelation_type=auto. The role's permissions flow through the edge — which specific operations are allowed is determined by the role, not the edge type. No separate RBAC check is needed for traversal. - Ref edges are stored per-instance in
association_scopes_entitieswithrelation_type=ref. If you have CRUD on parent A, you get READ-only on child B. CUD on B is not permitted via this path. The parent can list its ref children, but no permission delegation occurs. Further traversal from B requires a separate guarded-level permission check. - A
RelationTypeenum (auto,ref) is defined in code. Theassociation_scopes_entitiestable includes arelation_typecolumn to distinguish auto from ref edges.
- Reference Type (separate GQL types for ref entities): Rejected. Ref entities expose all fields.
- Denormalization (flat fields): Rejected. Increases maintenance burden.
Auto edges represent composition relationships with permission delegation via association_scopes_entities. If A ━━auto━━► B:
- The role's permissions flow through the edge to B. Which operations are permitted on B is determined by the role, not the edge type.
- No separate RBAC check is needed for B.
- B appears as a GQL sub-field of A.
Ref edges represent read-only references stored per-instance in association_scopes_entities with relation_type=ref, but with no permission delegation. If A ──ref──► B:
- A's CRUD permissions grant READ-only on B. CUD on B is not permitted via this path.
- The parent can list its ref children (the row in
association_scopes_entitiesenables this). - B appears as a read-only GQL sub-field of A.
- Further traversal from B requires a separate guarded-level permission check (same as accessing an independent entity).
Entity sharing (e.g., VFolder invitation) uses ref edges combined with entity-scope permissions:
- Ref edge provides visibility (listing) and prevents permission escalation from the invitee's scope.
- Entity-scope permissions control the exact operations granted (read, write, etc.).
When User A shares VFolder X with User B (write invitation):
association_scopes_entities:
(scope=User:B, entity=VFolder:X, relation_type=ref) ← visibility + escalation prevention
permissions (B's system role, entity-scope):
(scope=VFolder:X, entity_type=vfolder, op=read) ← explicit read grant
(scope=VFolder:X, entity_type=vfolder, op=write) ← explicit write grant
Permission check is two-layer:
- Entity-scope direct match (priority): Check if the user has a permission where the full scope identifier
(scope_type, scope_id)matches the target entity's type and id. This matches regardless of edge type. - CTE scope chain (fallback): Traverse
association_scopes_entitiesupward. Ref edges limit inherited permissions to READ-only, preventing the invitee's User-scope CRUD from escalating to the shared entity.
This ensures that B's existing User-scope permissions (e.g., vfolder:delete at scope=User:B) do not flow through to VFolder X, while explicitly granted entity-scope permissions (read/write) work as intended.
For the complete list of all auto, ref, and guarded edges, see Entity Edge Catalog.
- Unified edge storage.
association_scopes_entitiesis the single source of truth for all entity relationships, replacing scattered junction tables. It stores both permission delegation (auto) and visibility (ref) through therelation_typecolumn. - Edge property, not entity property. The relationship type is determined by the parent→child edge, not the entity itself. The same entity can be
autofrom one parent andreffrom another (e.g., Agent isautofrom ResourceGroup butreffrom Session).
These entities have standalone Root Queries with RBAC checks. Only these entities are stored in association_scopes_entities as scope→entity mappings.
Scoped:
- SessionRow, VFolderRow, EndpointRow, KeyPairRow, NotificationChannelRow
- NetworkRow, ScalingGroupRow, ContainerRegistryRow, StorageHostRow
- ImageRow, ArtifactRow, SessionTemplateRow
- UserRow, ProjectRow, AppConfigRow
Superadmin-only:
- DomainRow, ResourcePresetRow, UserResourcePolicyRow, KeyPairResourcePolicyRow, ProjectResourcePolicyRow, RoleRow, AuditLogRow, EventLogRow
These entities have mutation APIs (Create, Update, Delete, Purge). All root-query-enabled entities except read-only system logs.
Scoped:
- SessionRow, VFolderRow, EndpointRow, KeyPairRow, NotificationChannelRow
- NetworkRow, ScalingGroupRow, ContainerRegistryRow, StorageHostRow
- ImageRow, ArtifactRow, SessionTemplateRow
- UserRow, ProjectRow, AppConfigRow
Superadmin-only:
- DomainRow, ResourcePresetRow, UserResourcePolicyRow, KeyPairResourcePolicyRow, ProjectResourcePolicyRow, RoleRow
Read-only (no mutation API):
- AuditLogRow, EventLogRow
No standalone single-item or list queries. Always accessed through parent:
| Entity | Parent (auto edge) | Access Pattern |
|---|---|---|
| KernelRow | Session, Agent | session { kernels } |
| RoutingRow | Session, Endpoint | session { routings } |
| SessionDependencyRow | Session | session { dependencies } |
| SessionSchedulingHistoryRow | Session | session { schedulingHistory } |
| AgentRow | ResourceGroup | resourceGroup { agents } |
| ImageAliasRow | Image | image { aliases } |
| VFolderInvitationRow | VFolder | vfolder { invitations } |
| EndpointTokenRow | Endpoint | endpoint { tokens } |
| EndpointAutoScalingRuleRow | Endpoint | endpoint { autoScalingRules } |
| DeploymentRevisionRow | Endpoint | endpoint { revisions } |
| DeploymentPolicyRow | Endpoint | endpoint { policy } |
| DeploymentAutoScalingPolicyRow | Endpoint | endpoint { autoScalingPolicy } |
| DeploymentHistoryRow | Endpoint | endpoint { deploymentHistory } |
| ArtifactRevisionRow | Artifact | artifact { revisions } |
| NotificationRuleRow | NotificationChannel | notificationChannel { rules } |
| KernelSchedulingHistoryRow | Kernel | kernel { schedulingHistory } |
| RouteHistoryRow | Routing | routing { history } |
| DomainFairShareRow | Domain, ResourceGroup | domain { fairShare }, resourceGroup { domainFairShares } |
| ProjectFairShareRow | Project, ResourceGroup | project { fairShare }, resourceGroup { projectFairShares } |
| UserFairShareRow | User, ResourceGroup | user { fairShare }, resourceGroup { userFairShares } |
| PermissionRow | Role | role { permissions } |
| UserRoleRow | Role | role { userRoles } |
The following entities are intentionally excluded from the 3-Type Model:
System Internal / Infrastructure:
- ResourceSlotTypeRow, AgentResourceRow, ResourceAllocationRow (scheduler internals)
- ServiceCatalogRow, ServiceCatalogEndpointRow (internal service discovery)
- StorageNamespaceRow (ObjectStorage internal partitioning)
- EntityFieldRow (RBAC metadata)
- ErrorLogRow (system error log)
Registry Implementations (accessed via Artifact ref):
- HuggingFaceRegistryRow, ReservoirRegistryRow, ArtifactRegistryRow
StorageHost is a planned entity that does not yet exist as a DB table:
- Current state:
VFolderRow.hoststores a"proxy_name:volume_name"string.allowed_vfolder_hostsis a JSON column scattered across Domain/Group/KeyPairResourcePolicy. - Plan: Normalize into a
StorageHostRowDB table, unifying existingObjectStorageRowandVFSStorageRow. Positioned like ContainerRegistry as a root-query-enabled entity with N:N scope mapping viaassociation_scopes_entities.
- New table:
association_scopes_entities— stores per-instance auto and ref edges with arelation_typecolumn (autofor permission delegation,reffor read-only listing). - Remove:
permission_groupstable — fields (role_id,scope_type,scope_id) moved directly intopermissions. - Remove:
object_permissionstable — replaced by entity-as-scope pattern.
Existing junction tables will be replaced by association_scopes_entities.
These are N:N scope-accessibility mappings — entity visibility propagates to child scopes via CTE scope chain traversal (see Entity Edge Catalog for details).
| Current Table | Edge | relation_type |
|---|---|---|
ScalingGroupForDomainRow |
Domain ━━auto━━► ResourceGroup | auto |
ScalingGroupForProjectRow |
Project ━━auto━━► ResourceGroup | auto |
ScalingGroupForKeypairsRow |
User ━━auto━━► ResourceGroup | auto |
| (new) | Domain ━━auto━━► ContainerRegistry | auto |
AssociationContainerRegistriesGroupsRow |
Project ━━auto━━► ContainerRegistry | auto |
AssocGroupUserRow |
(sunset — replaced by entity_type='user' entries auto-synced on role assign/unassign) |
auto |
VFolderPermissionRow |
User ━━ref━━► VFolder (+ entity-scope permissions) | ref |
After migration, the core RBAC tables are:
rolesuser_rolespermissions(withrole_id,scope_type,scope_iddirectly)association_scopes_entities
- Sharing is implemented via ref edge + entity-scope permissions:
- Invite: INSERT ref edge in
association_scopes_entities+ INSERT entity-scope permissions (read/write) in the invitee's system role. - Revoke: DELETE ref edge + DELETE entity-scope permissions.
- The ref edge prevents the invitee's User-scope CRUD from escalating to the shared entity. Only explicitly granted entity-scope permissions apply.
- Invite: INSERT ref edge in
- Project membership is stored directly in
association_scopes_entities(entity_type='user'), auto-synced when roles are assigned/unassigned. This replaces the previousassociation_groups_userstable and enables single-table membership lookups.
-
Phase 1: Core Schema
- Create
association_scopes_entitiestable - Migrate
permissionstable to includerole_id,scope_type,scope_iddirectly - Remove
permission_groupsandobject_permissionstables
- Create
-
Phase 2: Entity Classification Metadata
- Define
RelationTypeenum in code - Annotate all entity types with their relationship metadata (auto/ref edges)
- Implement CTE-based scope chain traversal for guarded lookups
- Define
-
Phase 3: Root Query RBAC Enforcement
- Integrate guarded checks into Root Query resolvers
- Implement auto-traversal and ref auth context reset in nested resolvers
-
Phase 4: Association Table Migration
- Migrate existing junction tables to
association_scopes_entities - Remove deprecated junction tables
- Migrate StorageHost from string-based to normalized DB table
- Migrate existing junction tables to