Skip to content

Commit 8d236d4

Browse files
authored
[AAP-45443] Allow filtering by ansible_id (#734)
### What is being changed? Allows API query filtering by ansible id `user_ansible_id` `team_ansible_id` `object_ansible_id` Note, filtering with object_ansible_id accrues an additional database query for the Resource model. Example API calls ``` /v1/role_user_assignments/?user_ansible_id=<uuid> /v1/role_user_assignments/?object_ansible_id=<uuid> /v1/role_team_assignments/?team_ansible_id=<uuid> ``` query params can be combined where appropriate #### Validation Adds some validation if the provided ansible_id is not a UUID ``` [ "Invalid UUID format for user_ansible_id: 80c7e291-b121-48fc-" ] ``` ### Why is this change needed? Currently there is no way to filter on these fields, so working with the API becomes cumbersome and inefficient ### How does this change address the issue? Adds a django viewset filter backend to alias the query param to the appropriate queryset call. e.g. `user_ansible_id` applies a `user__resource__ansible_id` query ## Type of Change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [x] Test update - [ ] Refactoring (no functional changes) - [ ] Development environment change - [ ] Configuration change ## Self-Review Checklist - [x] I have performed a self-review of my code - [x] I have added relevant comments to complex code sections - [ ] I have updated documentation where needed - [x] I have considered the security impact of these changes - [x] I have considered performance implications - [x] I have thought about error handling and edge cases - [x] I have tested the changes in my local environment --------- Signed-off-by: Seth Foster <[email protected]>
1 parent b581c35 commit 8d236d4

File tree

3 files changed

+168
-0
lines changed

3 files changed

+168
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import uuid
2+
3+
from rest_framework.exceptions import ValidationError
4+
from rest_framework.filters import BaseFilterBackend
5+
6+
from ansible_base.resource_registry.models import Resource
7+
8+
9+
class AnsibleIdAliasFilterBackend(BaseFilterBackend):
10+
'''
11+
Filter backend for object_ansible_id.
12+
Note that this accrues an additional query to the Resource model.
13+
14+
Example:
15+
/api/v1/role_user_assignments/?object_ansible_id=da0488f5-013b-460c-8a62-c3c10a1d0fad
16+
'''
17+
18+
def filter_queryset(self, request, queryset, view):
19+
object_ansible_id = request.query_params.get('object_ansible_id')
20+
if object_ansible_id:
21+
try:
22+
# Validate if the provided ansible_id is a valid UUID
23+
uuid.UUID(object_ansible_id)
24+
except ValueError:
25+
raise ValidationError(f"Invalid UUID format for object_ansible_id: {object_ansible_id}")
26+
27+
try:
28+
# Find the Resource object by its ansible_id
29+
resource_obj = Resource.objects.get(ansible_id=object_ansible_id)
30+
31+
# Filter the queryset based on the resource's content_type and object_id
32+
queryset = queryset.filter(
33+
object_role__content_type=resource_obj.content_type, object_role__object_id=str(resource_obj.object_id) # Ensure object_id is string
34+
)
35+
except Resource.DoesNotExist:
36+
# If the resource is not found, return an empty queryset
37+
return queryset.none()
38+
39+
return queryset
40+
41+
42+
class UserAnsibleIdAliasFilterBackend(AnsibleIdAliasFilterBackend):
43+
"""
44+
Filter backend for user_ansible_id and object_ansible_id.
45+
46+
Example:
47+
/api/v1/role_user_assignments/?user_ansible_id=80c7e291-b121-48fc-8fb1-174aac6f57a6
48+
/api/v1/role_user_assignments/?object_ansible_id=da0488f5-013b-460c-8a62-c3c10a1d0fad
49+
"""
50+
51+
def filter_queryset(self, request, queryset, view):
52+
user_ansible_id = request.query_params.get('user_ansible_id')
53+
if user_ansible_id:
54+
try:
55+
# Validate if the provided ansible_id is a valid UUID
56+
uuid.UUID(user_ansible_id)
57+
except ValueError:
58+
raise ValidationError(f"Invalid UUID format for user_ansible_id: {user_ansible_id}")
59+
# Filter the queryset based on the user's ansible_id
60+
queryset = queryset.filter(user__resource__ansible_id=user_ansible_id)
61+
return super().filter_queryset(request, queryset, view)
62+
63+
64+
class TeamAnsibleIdAliasFilterBackend(AnsibleIdAliasFilterBackend):
65+
"""
66+
Filter backend for team_ansible_id and object_ansible_id.
67+
68+
Example:
69+
/api/v1/role_team_assignments/?team_ansible_id=c2b59b42-a874-43ca-9e1f-abe410864f65
70+
/api/v1/role_team_assignments/?object_ansible_id=da0488f5-013b-460c-8a62-c3c10a1d0fad
71+
"""
72+
73+
def filter_queryset(self, request, queryset, view):
74+
team_ansible_id = request.query_params.get('team_ansible_id')
75+
if team_ansible_id:
76+
try:
77+
# Validate if the provided ansible_id is a valid UUID
78+
uuid.UUID(team_ansible_id)
79+
except ValueError:
80+
raise ValidationError(f"Invalid UUID format for team_ansible_id: {team_ansible_id}")
81+
# Filter the queryset based on the team's ansible_id
82+
queryset = queryset.filter(team__resource__ansible_id=team_ansible_id)
83+
return super().filter_queryset(request, queryset, view)

ansible_base/rbac/api/views.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from ansible_base.lib.utils.views.django_app_api import AnsibleBaseDjangoAppApiView
1414
from ansible_base.lib.utils.views.permissions import try_add_oauth2_scope_permission
15+
from ansible_base.rbac.api.filter_backends import TeamAnsibleIdAliasFilterBackend, UserAnsibleIdAliasFilterBackend
1516
from ansible_base.rbac.api.permissions import RoleDefinitionPermissions
1617
from ansible_base.rbac.api.serializers import (
1718
RoleDefinitionDetailSerializer,
@@ -159,6 +160,7 @@ class RoleTeamAssignmentViewSet(BaseAssignmentViewSet):
159160

160161
serializer_class = RoleTeamAssignmentSerializer
161162
prefetch_related = ('team',)
163+
filter_backends = (TeamAnsibleIdAliasFilterBackend,)
162164

163165

164166
class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
@@ -175,3 +177,4 @@ class RoleUserAssignmentViewSet(BaseAssignmentViewSet):
175177

176178
serializer_class = RoleUserAssignmentSerializer
177179
prefetch_related = ('user',)
180+
filter_backends = (UserAnsibleIdAliasFilterBackend,)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
from django.contrib.contenttypes.models import ContentType
3+
from django.utils.http import urlencode
4+
5+
from ansible_base.lib.utils.response import get_relative_url
6+
from ansible_base.resource_registry.models import Resource
7+
8+
9+
class TestAnsibleIdAliasFilterBackend:
10+
11+
@pytest.mark.django_db
12+
def test_filter_user_ansible_id(self, admin_api_client, org_inv_rd, inv_rd, inventory, rando, organization):
13+
'''
14+
Test filtering RoleUserAssignment by user_ansible_id and object_ansible_id.
15+
'''
16+
# user - org assigment
17+
user_resource = Resource.objects.get(object_id=rando.pk, content_type=ContentType.objects.get_for_model(rando).pk)
18+
organization_resource = Resource.objects.get(object_id=organization.pk, content_type=ContentType.objects.get_for_model(organization).pk)
19+
url = get_relative_url('roleuserassignment-list')
20+
data = dict(role_definition=org_inv_rd.id, content_type='shared.organization', user_ansible_id=user_resource.ansible_id, object_id=organization.id)
21+
response = admin_api_client.post(url, data=data, format="json")
22+
assert response.status_code == 201, response.data
23+
24+
# user - inventory assignment (just a random assignment to make total count > 1)
25+
data = dict(role_definition=inv_rd.id, content_type='shared.inventory', user_ansible_id=user_resource.ansible_id, object_id=inventory.id)
26+
response = admin_api_client.post(url, data=data, format="json")
27+
assert response.status_code == 201, response.data
28+
29+
# make sure > 1 assignments total to ensure filtering is not returning undesired results
30+
response = admin_api_client.get(url)
31+
assert response.data["count"] > 1, response.data
32+
33+
# filter by user_ansible_id
34+
query_params = {'user_ansible_id': user_resource.ansible_id}
35+
response = admin_api_client.get(url + '?' + urlencode(query_params))
36+
assert response.status_code == 200, response.data
37+
assert response.data["count"] == 2, response.data
38+
39+
# filter by object_ansible_id
40+
query_params = {'object_ansible_id': organization_resource.ansible_id}
41+
response = admin_api_client.get(url + '?' + urlencode(query_params))
42+
assert response.status_code == 200, response.data
43+
assert response.data["count"] == 1, response.data
44+
45+
# filter by both user_ansible_id and object_ansible_id
46+
query_params = {'user_ansible_id': user_resource.ansible_id, 'object_ansible_id': organization_resource.ansible_id}
47+
response = admin_api_client.get(url + '?' + urlencode(query_params))
48+
assert response.status_code == 200, response.data
49+
assert response.data["count"] == 1, response.data
50+
51+
@pytest.mark.django_db
52+
def test_filter_team_ansible_id(self, admin_api_client, team, inv_rd, inventory):
53+
'''
54+
Test filtering RoleTeamAssignment by team_ansible_id.
55+
'''
56+
team_resource = Resource.objects.get(object_id=team.pk, content_type=ContentType.objects.get_for_model(team).pk)
57+
url = get_relative_url('roleteamassignment-list')
58+
data = dict(role_definition=inv_rd.id, content_type='shared.organization', team_ansible_id=team_resource.ansible_id, object_id=inventory.id)
59+
response = admin_api_client.post(url, data=data, format="json")
60+
assert response.status_code == 201, response.data
61+
62+
# filter by team_ansible_id
63+
query_params = {'team_ansible_id': team_resource.ansible_id}
64+
response = admin_api_client.get(url + '?' + urlencode(query_params))
65+
assert response.status_code == 200, response.data
66+
assert response.data["count"] == 1, response.data
67+
68+
@pytest.mark.parametrize("ansible_id_type", ["user", "team", "object"])
69+
@pytest.mark.django_db
70+
def test_invalid_ansible_id_format(self, admin_api_client, ansible_id_type):
71+
'''
72+
Test that invalid UUID formats for ansible_id raise a 400 error.
73+
'''
74+
if ansible_id_type == "team":
75+
actor = "team"
76+
else:
77+
actor = "user"
78+
url = get_relative_url(f'role{actor}assignment-list')
79+
query_params = {f'{ansible_id_type}_ansible_id': 'invalid-uuid-format'}
80+
response = admin_api_client.get(url + '?' + urlencode(query_params))
81+
assert response.status_code == 400, response.data
82+
assert "Invalid UUID format for" in str(response.data), response.data

0 commit comments

Comments
 (0)