Skip to content

Conversation

fregataa
Copy link
Member

@fregataa fregataa commented Sep 22, 2025

resolves #6003 (BA-2478)

Checklist: (if applicable)

  • Milestone metadata specifying the target backport version
  • Mention to the original issue
  • Installer updates including:
    • Fixtures for db schema changes
    • New mandatory config options
  • Update of end-to-end CLI integration tests in ai.backend.test
  • API server-client counterparts (e.g., manager API -> client SDK)
  • Test case(s) to:
    • Demonstrate the difference of before/after
    • Demonstrate the flow of abstract/conceptual models with a concrete implementation
  • Documentation
    • Contents in the docs directory
    • docstrings in public interfaces and type annotations

@fregataa fregataa added this to the 25Q2 milestone Sep 22, 2025
@fregataa fregataa self-assigned this Sep 22, 2025
@Copilot Copilot AI review requested due to automatic review settings September 22, 2025 13:10
@github-actions github-actions bot added size:L 100~500 LoC comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated labels Sep 22, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds a new GLOBAL RBAC scope and introduces system-level superadmin and monitor roles, along with a migration to create and map these roles to existing users. Key changes include extending enums with a GLOBAL scope, adding role creation helpers, and an Alembic migration to seed global-scope roles and permissions.

  • Added GLOBAL scope and related constants to RBAC enums and permission types
  • Introduced superadmin and monitor role creation helpers
  • New migration to create global roles, permission groups, and map users by legacy role

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
models/rbac_models/migration/user.py Adds constants and helper functions for superadmin and monitor role creation
models/rbac_models/migration/enums.py Extends OperationType and ScopeType with monitor/global concepts and introduces GLOBAL_SCOPE_ID
models/alembic/versions/09206ac04fd3_create_global_scope_roles.py Migration to create global-scope roles, permissions, and map users
data/permission/types.py Adds GLOBAL scope and duplicates GLOBAL_SCOPE_ID constant

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

OperationType.GRANT_READ,
OperationType.GRANT_UPDATE,
}
SUPERADMIN_OPERATIONS = OPERATIONS_IN_ROLE
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] SUPERADMIN_OPERATIONS references the same set object as OPERATIONS_IN_ROLE; if either is mutated later, both change unintentionally. Use a shallow copy to decouple: SUPERADMIN_OPERATIONS = OPERATIONS_IN_ROLE.copy().

Suggested change
SUPERADMIN_OPERATIONS = OPERATIONS_IN_ROLE
SUPERADMIN_OPERATIONS = OPERATIONS_IN_ROLE.copy()

Copilot uses AI. Check for mistakes.

Comment on lines +75 to +82
def get_superadmin_role_creation_input() -> RoleCreateInput:
"""
Create a superadmin role and permissions.
This role allows full access to all entities.
"""
role_input = RoleCreateInput(
name=f"{ROLE_NAME_PREFIX}superadmin",
source=RoleSource.SYSTEM,
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring states this creates the role and permissions, but the function only returns a RoleCreateInput without creating permissions. Either adjust the wording (e.g., "Return RoleCreateInput for superadmin") or implement permission creation here for consistency.

Copilot uses AI. Check for mistakes.

Comment on lines +87 to +93
def get_monitor_role_creation_input() -> RoleCreateInput:
"""
Create a monitor role and permissions.
This role allows read-only access to all entities.
"""
role_input = RoleCreateInput(
name=f"{ROLE_NAME_PREFIX}monitor",
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same mismatch as superadmin helper: the function returns only a RoleCreateInput and does not create permissions. Update docstring or extend functionality for clarity and consistency.

Copilot uses AI. Check for mistakes.

return OriginalScopeType(self.value)


GLOBAL_SCOPE_ID = ""
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] GLOBAL_SCOPE_ID is an empty string with no explanatory comment; using a sentinel empty string can be error-prone (e.g., falsy checks). Provide a comment explaining why an empty string is valid and consider using a clearly descriptive constant (e.g., "GLOBAL") to reduce ambiguity.

Suggested change
GLOBAL_SCOPE_ID = ""
# Sentinel value for the global scope. Use a descriptive string to avoid ambiguity and errors with falsy checks.
GLOBAL_SCOPE_ID = "GLOBAL"

Copilot uses AI. Check for mistakes.

GLOBAL = "global" # Global scope


GLOBAL_SCOPE_ID = ""
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] GLOBAL_SCOPE_ID duplicates the same empty-string constant defined elsewhere (enums module). Centralize this definition to avoid divergence and document why an empty string is used.

Suggested change
GLOBAL_SCOPE_ID = ""
from ai.backend.manager.data.enums import GLOBAL_SCOPE_ID # Centralized definition; see enums module for documentation.

Copilot uses AI. Check for mistakes.

Comment on lines 90 to 96
input = (
PermissionGroupCreateInput(
role_id=role_id,
scope_type=ScopeType.GLOBAL,
scope_id=GLOBAL_SCOPE_ID,
)
).to_dict()
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable name input shadows the built-in function input, which can confuse readers and tools. Rename to permission_group_data or pg_input for clarity.

Copilot uses AI. Check for mistakes.

Comment on lines 120 to 125
input = PermissionCreateInput(
permission_group_id=permission_group_id,
entity_type=entity_type,
operation=operation,
)
permission_inputs.append(input.to_dict())
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same shadowing of built-in input here; rename variable (e.g., perm_input) to avoid overshadowing and improve readability.

Copilot uses AI. Check for mistakes.

class UserRole(enum.StrEnum):
"""
User's role.
"""

SUPERADMIN = "superadmin"
ADMIN = "admin"
USER = "user"
MONITOR = "monitor"
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Redefining UserRole inside the migration duplicates existing role definitions elsewhere, increasing risk of drift if roles change. Consider importing the canonical enum or inlining plain string literals to avoid maintaining parallel enums in migrations.

Copilot uses AI. Check for mistakes.



def downgrade() -> None:
pass
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing downgrade logic means this migration is irreversible, which can hinder debugging or rollback scenarios. Implement removal of inserted roles, permission groups, permissions, and user-role mappings (or explicitly document why downgrade is intentionally unsupported).

Suggested change
pass
conn = op.get_bind()
roles_table = get_roles_table()
user_roles_table = get_user_roles_table()
permissions_table = get_permissions_table()
permission_groups_table = get_permission_groups_table()
# List of role names to remove
role_names = [UserRole.SUPERADMIN, UserRole.MONITOR]
# Find role IDs for these roles
role_rows = query_role_rows_by_name(conn, role_names)
role_ids = [row.id for row in role_rows]
if role_ids:
# Remove user-role mappings
conn.execute(
user_roles_table.delete().where(
user_roles_table.c.role_id.in_(role_ids)
)
)
# Remove permissions associated with these roles
conn.execute(
permissions_table.delete().where(
permissions_table.c.role_id.in_(role_ids)
)
)
# Remove permission groups associated with these roles
conn.execute(
permission_groups_table.delete().where(
permission_groups_table.c.role_id.in_(role_ids)
)
)
# Remove the roles themselves
conn.execute(
roles_table.delete().where(
roles_table.c.id.in_(role_ids)
)
)

Copilot uses AI. Check for mistakes.

Comment on lines +89 to +92
"""
return {
cls.READ,
}
Copy link

Copilot AI Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] monitor_operations duplicates member_operations logic (both return only READ). If the semantic distinction is intentional, add a clarifying comment; otherwise consider reusing member_operations to avoid duplication.

Suggested change
"""
return {
cls.READ,
}
This method reuses member_operations for consistency and to avoid duplication.
"""
return cls.member_operations()

Copilot uses AI. Check for mistakes.

@fregataa fregataa marked this pull request as draft September 22, 2025 13:21
@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch 2 times, most recently from 7e5435c to c5407ac Compare September 24, 2025 01:43
Comment on lines 23 to 24
def is_global(self) -> bool:
return self.scope_type == ScopeType.GLOBAL
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m not sure if this is really meaningful.

Comment on lines 76 to 82
@dataclass
class PermissionCheckInput:
class MergedPermissionData:
scope_permissions: Mapping[ScopeId, set[OperationType]]
global_permissions: set[OperationType]
object_permissions: Mapping[ObjectId, set[OperationType]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like it would be good to separate it. However, I think we need to consider how to separate it a little bit, so I'll take a look and suggest some ideas.

@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch from c5407ac to d200694 Compare September 26, 2025 13:16
@fregataa fregataa marked this pull request as ready for review September 26, 2025 13:16
@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch from d0e4b56 to 88a06b5 Compare September 29, 2025 04:42
@HyeockJinKim HyeockJinKim modified the milestones: 25Q2, 25.16 Oct 4, 2025
@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch from 88a06b5 to bd85f26 Compare October 13, 2025 06:50
@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch 2 times, most recently from 7c18773 to 46daf25 Compare October 20, 2025 12:25
scope_id: str

def __hash__(self) -> int:
return hash((self.scope_type, self.scope_id))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about setting the frozen attribute of the dataclass?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems better to make values immutable rather than making mutable values hashable.

Comment on lines 100 to 119
@dataclass
class BatchEntityPermissionCheckInput:
user_id: uuid.UUID
target_object_ids: list[ObjectId]
operation: OperationType


@dataclass
class ScopePermissionSet:
scope_id: ScopeId
scope_permissions: set[OperationType]
global_permissions: Optional[set[OperationType]]


@dataclass
class ObjectPermissionSet:
object_id: ObjectId
object_permissions: set[OperationType]
mapped_scopes: dict[ScopeId, set[OperationType]]
global_permissions: Optional[set[OperationType]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to keep most of the values frozen.

Comment on lines 166 to 169
sa.or_(
PermissionGroupRow.scope_type == ScopeType.GLOBAL,
PermissionGroupRow.scope_id == scope_id.scope_id,
),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check the scope type?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also receive the value for the operation.

Comment on lines 182 to 193
scope_permissions: set[OperationType] = set()
global_permissions: Optional[set[OperationType]] = None
for role in role_rows:
for pg in role.permission_group_rows:
if pg.parsed_scope_id() != scope_id:
continue
if pg.scope_type == ScopeType.GLOBAL:
global_permissions = {perm.operation for perm in pg.permission_rows}
else:
scope_permissions |= {perm.operation for perm in pg.permission_rows}

return ScopePermissionSet(scope_id, scope_permissions, global_permissions)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be a need to separate global permission specifically. What's actually needed is whether or not there's permission for the operation.

Comment on lines +195 to +153
async def check_scope_permission_exist(
self,
user_id: uuid.UUID,
scope_id: ScopeId,
operation: OperationType,
) -> bool:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are returning the permission set anyway, it doesn't seem necessary to have a separate check function.

Comment on lines 103 to 105
@permission_controller_repository_resilience.apply()
async def check_permission_in_scope(self, data: ScopePermissionCheckInput) -> bool:
target_scope_id = data.target_scope_id
role_rows = await self._db_source.get_user_roles(data.user_id)
for role in role_rows:
for permission_group in role.permission_group_rows:
if permission_group.parsed_scope_id() != target_scope_id:
continue
for permission in permission_group.permission_rows:
if permission.operation == data.operation:
return True
return await self._db_source.check_scope_permission_exist(
data.user_id, data.target_scope_id, data.operation
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to the situation where a considerable amount of computation is performed in the DB, a cache layer is essential. As a follow-up task, please also apply a cache source.

Comment on lines 121 to 122

GLOBAL = "global" # Global scope
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove line breaks.

Comment on lines 267 to 271
async def get_object_permissions(
self,
user_id: uuid.UUID,
object_id: ObjectId,
) -> ObjectPermissionSet:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You missed resilience decorator.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a db source module, not a repository module

@fregataa fregataa force-pushed the feat/add-rbac-global-scope branch from 46daf25 to e5d55cd Compare October 21, 2025 09:07
@fregataa fregataa requested a review from HyeockJinKim October 21, 2025 09:27
@github-actions github-actions bot added size:XL 500~ LoC and removed size:L 100~500 LoC labels Oct 21, 2025
@HyeockJinKim HyeockJinKim added this pull request to the merge queue Oct 21, 2025
auto-merge was automatically disabled October 21, 2025 09:56

Pull Request is not mergeable

Merged via the queue into main with commit ca26b1f Oct 21, 2025
31 checks passed
@HyeockJinKim HyeockJinKim deleted the feat/add-rbac-global-scope branch October 21, 2025 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp:manager Related to Manager component require:db-migration Automatically set when alembic migrations are added or updated size:XL 500~ LoC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Global Scope to RBAC Implementation

2 participants