Skip to content

Commit 170f2a3

Browse files
fregataaclaudelablup-octodog
authored andcommitted
feat(BA-5769): add my-role SDK, CLI, and v2 component tests (#11179)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: octodog <mu001@lablup.com>
1 parent da678c2 commit 170f2a3

18 files changed

Lines changed: 511 additions & 46 deletions

File tree

changes/11179.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add my-role REST v2 SDK endpoint and CLI command for users to query their own role assignments

docs/manager/graphql-reference/supergraph.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14857,7 +14857,7 @@ type RoleAssignmentEdge
1485714857
input RoleAssignmentFilter
1485814858
@join__type(graph: STRAWBERRY)
1485914859
{
14860-
roleId: UUID = null
14860+
roleId: UUIDFilter = null
1486114861
role: RoleAssignmentRoleNestedFilter = null
1486214862
permission: PermissionNestedFilter = null
1486314863
username: StringFilter = null

docs/manager/graphql-reference/v2-schema.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9809,7 +9809,7 @@ type RoleAssignmentEdge {
98099809

98109810
"""Added in 26.3.0. Filter for role assignments"""
98119811
input RoleAssignmentFilter {
9812-
roleId: UUID = null
9812+
roleId: UUIDFilter = null
98139813
role: RoleAssignmentRoleNestedFilter = null
98149814
permission: PermissionNestedFilter = null
98159815
username: StringFilter = null

src/ai/backend/client/cli/v2/my/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def keypair() -> None:
2121
"""My keypair commands."""
2222

2323

24+
@my.group(cls=LazyGroup, import_name="ai.backend.client.cli.v2.my.role:role")
25+
def role() -> None:
26+
"""My role commands."""
27+
28+
2429
@my.group(
2530
cls=LazyGroup,
2631
import_name="ai.backend.client.cli.v2.my.login_history:login_history",
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""CLI commands for self-service role operations."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
from uuid import UUID
7+
8+
import click
9+
10+
from ai.backend.client.cli.v2.helpers import (
11+
create_v2_registry,
12+
load_v2_config,
13+
parse_order_options,
14+
print_result,
15+
)
16+
17+
18+
@click.group()
19+
def role() -> None:
20+
"""My role commands."""
21+
22+
23+
@role.command()
24+
@click.option("--limit", default=None, type=int, help="Maximum number of results to return.")
25+
@click.option("--offset", default=None, type=int, help="Number of results to skip.")
26+
@click.option("--first", default=None, type=int, help="Cursor-based: return first N items.")
27+
@click.option("--after", default=None, type=str, help="Cursor-based: return items after cursor.")
28+
@click.option("--last", default=None, type=int, help="Cursor-based: return last N items.")
29+
@click.option("--before", default=None, type=str, help="Cursor-based: return items before cursor.")
30+
@click.option("--role-id", type=click.UUID, default=None, help="Filter by role UUID.")
31+
@click.option(
32+
"--order-by",
33+
multiple=True,
34+
help="Order by field:direction (e.g., granted_at:desc).",
35+
)
36+
def search(
37+
limit: int | None,
38+
offset: int | None,
39+
first: int | None,
40+
after: str | None,
41+
last: int | None,
42+
before: str | None,
43+
role_id: UUID | None,
44+
order_by: tuple[str, ...],
45+
) -> None:
46+
"""Search my role assignments."""
47+
from ai.backend.common.dto.manager.query import UUIDFilter
48+
from ai.backend.common.dto.manager.v2.rbac.request import (
49+
RoleAssignmentFilter,
50+
RoleAssignmentOrderBy,
51+
SearchRoleAssignmentsInput,
52+
)
53+
from ai.backend.common.dto.manager.v2.rbac.types import RoleAssignmentOrderField
54+
55+
filter_dto: RoleAssignmentFilter | None = None
56+
if role_id is not None:
57+
filter_dto = RoleAssignmentFilter(
58+
role_id=UUIDFilter(equals=role_id),
59+
)
60+
61+
orders = (
62+
parse_order_options(order_by, RoleAssignmentOrderField, RoleAssignmentOrderBy)
63+
if order_by
64+
else None
65+
)
66+
67+
async def _run() -> None:
68+
registry = await create_v2_registry(load_v2_config())
69+
try:
70+
request = SearchRoleAssignmentsInput(
71+
filter=filter_dto,
72+
order=orders,
73+
first=first,
74+
after=after,
75+
last=last,
76+
before=before,
77+
limit=limit,
78+
offset=offset,
79+
)
80+
result = await registry.rbac.my_search_assignments(request)
81+
print_result(result)
82+
finally:
83+
await registry.close()
84+
85+
asyncio.run(_run())

src/ai/backend/client/cli/v2/rbac/assignment.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,31 +28,31 @@ def assignment() -> None:
2828
multiple=True,
2929
help="Order by field:direction (e.g., username:asc, granted_at:desc).",
3030
)
31-
@click.option("--role-id", type=str, default=None, help="Filter by role UUID.")
31+
@click.option("--role-id", type=click.UUID, default=None, help="Filter by role UUID.")
3232
@click.option("--username-contains", type=str, default=None, help="Filter by username (contains).")
3333
@click.option("--email-contains", type=str, default=None, help="Filter by email (contains).")
3434
def search(
3535
limit: int | None,
3636
offset: int | None,
3737
order_by: tuple[str, ...],
38-
role_id: str | None,
38+
role_id: UUID | None,
3939
username_contains: str | None,
4040
email_contains: str | None,
4141
) -> None:
4242
"""Search role assignments."""
43-
from ai.backend.common.dto.manager.query import StringFilter
43+
from ai.backend.common.dto.manager.query import StringFilter, UUIDFilter
4444
from ai.backend.common.dto.manager.v2.rbac.request import (
45-
AdminSearchRoleAssignmentsGQLInput,
4645
RoleAssignmentFilter,
4746
RoleAssignmentOrderBy,
47+
SearchRoleAssignmentsInput,
4848
)
4949
from ai.backend.common.dto.manager.v2.rbac.types import RoleAssignmentOrderField
5050

5151
# Build filter only if any filter option is provided
5252
filter_dto: RoleAssignmentFilter | None = None
5353
if any([role_id is not None, username_contains is not None, email_contains is not None]):
5454
filter_dto = RoleAssignmentFilter(
55-
role_id=UUID(role_id) if role_id is not None else None,
55+
role_id=UUIDFilter(equals=role_id) if role_id is not None else None,
5656
username=(
5757
StringFilter(contains=username_contains) if username_contains is not None else None
5858
),
@@ -70,7 +70,7 @@ async def _run() -> None:
7070
registry = await create_v2_registry(load_v2_config())
7171
try:
7272
result = await registry.rbac.search_assignments(
73-
AdminSearchRoleAssignmentsGQLInput(
73+
SearchRoleAssignmentsInput(
7474
filter=filter_dto,
7575
order=orders,
7676
limit=limit,

src/ai/backend/client/v2/domains_v2/rbac.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from ai.backend.common.dto.manager.v2.rbac.request import (
99
AdminSearchEntitiesGQLInput,
1010
AdminSearchPermissionsGQLInput,
11-
AdminSearchRoleAssignmentsGQLInput,
1211
AssignRoleInput,
1312
BulkAssignRoleInput,
1413
BulkRevokeRoleInput,
@@ -18,14 +17,14 @@
1817
DeleteRoleInput,
1918
PurgeRoleInput,
2019
RevokeRoleInput,
20+
SearchRoleAssignmentsInput,
2121
SearchRolesInput,
2222
UpdatePermissionInput,
2323
UpdateRoleInput,
2424
)
2525
from ai.backend.common.dto.manager.v2.rbac.response import (
2626
AdminSearchAssociationsPayload,
2727
AdminSearchPermissionsPayload,
28-
AdminSearchRoleAssignmentsPayload,
2928
AdminSearchRolesPayload,
3029
BulkAssignRoleResultPayload,
3130
BulkRevokeRoleResultPayload,
@@ -36,6 +35,7 @@
3635
PurgeRolePayload,
3736
RoleAssignmentNode,
3837
RoleNode,
38+
SearchRoleAssignmentsPayload,
3939
UpdateRolePayload,
4040
)
4141

@@ -172,14 +172,25 @@ async def revoke_role(self, request: RevokeRoleInput) -> RoleAssignmentNode:
172172
)
173173

174174
async def search_assignments(
175-
self, request: AdminSearchRoleAssignmentsGQLInput
176-
) -> AdminSearchRoleAssignmentsPayload:
175+
self, request: SearchRoleAssignmentsInput
176+
) -> SearchRoleAssignmentsPayload:
177177
"""Search role assignments with filters, orders, and pagination."""
178178
return await self._client.typed_request(
179179
"POST",
180180
f"{_PATH}/assignments/search",
181181
request=request,
182-
response_model=AdminSearchRoleAssignmentsPayload,
182+
response_model=SearchRoleAssignmentsPayload,
183+
)
184+
185+
async def my_search_assignments(
186+
self, request: SearchRoleAssignmentsInput
187+
) -> SearchRoleAssignmentsPayload:
188+
"""Search role assignments for the current authenticated user."""
189+
return await self._client.typed_request(
190+
"POST",
191+
f"{_PATH}/assignments/my/search",
192+
request=request,
193+
response_model=SearchRoleAssignmentsPayload,
183194
)
184195

185196
async def bulk_assign_role(self, request: BulkAssignRoleInput) -> BulkAssignRoleResultPayload:

src/ai/backend/common/dto/manager/v2/rbac/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from ai.backend.common.dto.manager.v2.rbac.request import (
66
AdminSearchEntitiesGQLInput,
77
AdminSearchPermissionsGQLInput,
8-
AdminSearchRoleAssignmentsGQLInput,
98
AssignRoleInput,
109
BulkAssignRoleInput,
1110
BulkRevokeRoleInput,
@@ -24,6 +23,7 @@
2423
RoleFilter,
2524
RoleNestedFilter,
2625
RoleOrderBy,
26+
SearchRoleAssignmentsInput,
2727
SearchRolesInput,
2828
UpdatePermissionInput,
2929
UpdateRoleInput,
@@ -93,7 +93,7 @@
9393
# Input models (request)
9494
"AdminSearchEntitiesGQLInput",
9595
"AdminSearchPermissionsGQLInput",
96-
"AdminSearchRoleAssignmentsGQLInput",
96+
"SearchRoleAssignmentsInput",
9797
"SearchRolesInput",
9898
"AssignRoleInput",
9999
"BulkAssignRoleInput",

src/ai/backend/common/dto/manager/v2/rbac/request.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from pydantic import Field, field_validator
1010

1111
from ai.backend.common.api_handlers import SENTINEL, BaseRequestModel, Sentinel
12-
from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter
12+
from ai.backend.common.dto.manager.query import DateTimeFilter, StringFilter, UUIDFilter
1313

1414
from .types import (
1515
OrderDirection,
@@ -23,7 +23,7 @@
2323
__all__ = (
2424
"AdminSearchEntitiesGQLInput",
2525
"AdminSearchPermissionsGQLInput",
26-
"AdminSearchRoleAssignmentsGQLInput",
26+
"SearchRoleAssignmentsInput",
2727
"SearchRolesInput",
2828
"AssignRoleInput",
2929
"BulkAssignRoleInput",
@@ -208,7 +208,7 @@ class PermissionNestedFilter(BaseRequestModel):
208208
class RoleAssignmentFilter(BaseRequestModel):
209209
"""Filter for role assignments."""
210210

211-
role_id: UUID | None = None
211+
role_id: UUIDFilter | None = None
212212
role: RoleNestedFilter | None = None
213213
permission: PermissionNestedFilter | None = None
214214
username: StringFilter | None = None
@@ -303,8 +303,8 @@ class SearchRolesInput(BaseRequestModel):
303303
offset: int | None = None
304304

305305

306-
class AdminSearchRoleAssignmentsGQLInput(BaseRequestModel):
307-
"""GQL pagination search input for role assignments."""
306+
class SearchRoleAssignmentsInput(BaseRequestModel):
307+
"""Pagination search input for role assignments."""
308308

309309
filter: RoleAssignmentFilter | None = None
310310
order: list[RoleAssignmentOrderBy] | None = None

src/ai/backend/common/dto/manager/v2/rbac/response.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
__all__ = (
2222
"AdminSearchAssociationsPayload",
2323
"AdminSearchPermissionsPayload",
24-
"AdminSearchRoleAssignmentsPayload",
24+
"SearchRoleAssignmentsPayload",
2525
"AdminSearchRolesPayload",
2626
"AssociationScopesEntitiesNode",
2727
"BulkAssignRoleFailureInfo",
@@ -183,7 +183,7 @@ class AdminSearchPermissionsPayload(BaseResponseModel):
183183
has_previous_page: bool = Field(description="Whether there is a previous page.")
184184

185185

186-
class AdminSearchRoleAssignmentsPayload(BaseResponseModel):
186+
class SearchRoleAssignmentsPayload(BaseResponseModel):
187187
"""Paginated result for role assignment search."""
188188

189189
items: list[RoleAssignmentNode] = Field(description="List of role assignment nodes.")

0 commit comments

Comments
 (0)