Skip to content

Commit 1516f17

Browse files
authored
Merge branch 'devel' into phase2/feature-flags/poc
2 parents 5387799 + 0e4879c commit 1516f17

File tree

2 files changed

+69
-10
lines changed

2 files changed

+69
-10
lines changed

ansible_base/rbac/api/views.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from rest_framework.viewsets import GenericViewSet, ModelViewSet, mixins
1515

1616
from ansible_base.lib.utils.auth import get_team_model, get_user_model
17+
from ansible_base.lib.utils.schema import extend_schema_if_available
1718
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
1819
from ansible_base.lib.utils.views.permissions import try_add_oauth2_scope_permission
1920
from ansible_base.rbac.api.permissions import RoleDefinitionPermissions
@@ -236,17 +237,24 @@ class RoleTeamAssignmentViewSet(BaseAssignmentViewSet):
236237
]
237238

238239

239-
class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
240-
"""
241-
Use this endpoint to give a user permission to a resource or an organization.
242-
The needed data is the user, the role definition, and the object id.
243-
The object must be of the type specified in the role definition.
244-
The type given in the role definition and the provided object_id are used
245-
to look up the resource.
240+
# Schema fragments for RoleUserAssignmentViewSet OpenAPI spec
241+
_USER_ACTOR_ONEOF = {
242+
'oneOf': [
243+
{'required': ['user'], 'not': {'required': ['user_ansible_id']}},
244+
{'required': ['user_ansible_id'], 'not': {'required': ['user']}},
245+
]
246+
}
247+
248+
_OBJECT_ID_ONEOF = {
249+
'oneOf': [
250+
{'properties': {'object_id': {'oneOf': [{'type': 'integer'}, {'type': 'string', 'format': 'uuid'}]}, 'object_ansible_id': False}},
251+
{'properties': {'object_ansible_id': {'type': 'string', 'format': 'uuid'}, 'object_id': False}},
252+
{'not': {'anyOf': [{'required': ['object_id']}, {'required': ['object_ansible_id']}]}},
253+
]
254+
}
246255

247-
After creation, the assignment cannot be edited, but can be deleted to
248-
remove those permissions.
249-
"""
256+
257+
class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
250258

251259
resource_purpose = "RBAC role grants assigning permissions to users for specific resources"
252260

@@ -257,6 +265,25 @@ class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
257265
ansible_id_backend.RoleAssignmentFilterBackend,
258266
]
259267

268+
@extend_schema_if_available(
269+
request={
270+
'application/json': {
271+
'allOf': [
272+
{'$ref': '#/components/schemas/RoleUserAssignment'},
273+
_USER_ACTOR_ONEOF,
274+
_OBJECT_ID_ONEOF,
275+
]
276+
},
277+
},
278+
description="Give a user permission to a resource, an organization, or globally (when allowed)."
279+
"Must specify 'role_definition' and exactly one of 'user' or 'user_ansible_id'."
280+
"Can specify at most one of 'object_id' or 'object_ansible_id' (omit both for global roles)."
281+
"The content_type of the role definition and the provided object_id are used to look up the resource."
282+
"After creation, the assignment cannot be edited, but can be deleted to remove those permissions.",
283+
)
284+
def create(self, request, *args, **kwargs):
285+
return super().create(request, *args, **kwargs)
286+
260287

261288
class AccessURLMixin:
262289
def get_actor_model(self):

test_app/tests/api_documentation/test_schema_integration.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,35 @@ def test_openapi_schema_unauthenticated_access(unauthenticated_api_client):
148148
if response.status_code == 200:
149149
schema = response.data
150150
assert 'openapi' in schema
151+
152+
153+
def test_role_user_assignment_create_schema(admin_api_client):
154+
"""
155+
Test that RoleUserAssignmentViewSet's create operation has proper schema constraints.
156+
157+
Generated by Claude Code (claude-sonnet-4-5@20250929)
158+
159+
Verifies that the request body schema properly enforces:
160+
- Exactly one of 'user' or 'user_ansible_id' is required
161+
- At most one of 'object_id' or 'object_ansible_id' can be specified
162+
"""
163+
url = '/api/v1/docs/schema/'
164+
response = admin_api_client.get(url)
165+
assert response.status_code == 200
166+
167+
# Navigate directly to the schema - will raise KeyError if path doesn't exist
168+
all_of = response.data['paths']['/api/v1/role_user_assignments/']['post']['requestBody']['content']['application/json']['schema']['allOf']
169+
170+
# Verify structure: [base_schema, user_constraint, object_constraint]
171+
assert len(all_of) == 3, "Should have 3 items: base schema + 2 constraint sets"
172+
assert all_of[0] == {'$ref': '#/components/schemas/RoleUserAssignment'}
173+
174+
# Verify user constraint: exactly one of 'user' or 'user_ansible_id'
175+
user_one_of = all_of[1]['oneOf']
176+
assert len(user_one_of) == 2
177+
assert user_one_of[0] == {'required': ['user'], 'not': {'required': ['user_ansible_id']}}
178+
assert user_one_of[1] == {'required': ['user_ansible_id'], 'not': {'required': ['user']}}
179+
180+
# Verify object constraint: at most one of 'object_id' or 'object_ansible_id'
181+
object_one_of = all_of[2]['oneOf']
182+
assert len(object_one_of) == 3

0 commit comments

Comments
 (0)