Skip to content

Commit

Permalink
Add callback to validate role assignment
Browse files Browse the repository at this point in the history
Some dab-based apps like AWX may wish to add
exceptions to the user and team role assignments.

This PR adds logic to the serializer
.create method to execute a callback method
that is optionally defined on the model.

This callback signature looks like:

validate_role_assignment(self, actor, role_definition)

and should return either
- None
- a string describing the validation error

The string will be wrapped up a PermissionDenied
error (HTTP 403)

Signed-off-by: Seth Foster <[email protected]>
  • Loading branch information
fosterseth committed Jun 25, 2024
1 parent ed8ab39 commit d11602a
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 0 deletions.
8 changes: 8 additions & 0 deletions ansible_base/rbac/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.db.utils import IntegrityError
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import flatten_choices_dict, to_choices_dict
Expand Down Expand Up @@ -242,6 +243,13 @@ def create(self, validated_data):
# Resolve object
obj = self.get_object_from_data(validated_data, rd, requesting_user)

# model-level callback to further validate the assignment
# can be optionally implemented by the model
if getattr(obj, 'validate_role_assignment', None):
errmsg = obj.validate_role_assignment(actor, rd)
if errmsg:
raise PermissionDenied(errmsg)

if rd.content_type:
# Object role assignment
if not obj:
Expand Down
6 changes: 6 additions & 0 deletions test_app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ class Meta:
def summary_fields(self):
return {"id": self.id, "name": self.name}

def validate_role_assignment(self, actor, role_definition):
if isinstance(actor, User) and actor.username == 'user-not-allowed':
return 'Role assignment not allowed'
if isinstance(actor, Team) and actor.name == 'team-not-allowed':
return 'Role assignment not allowed'


class Credential(models.Model):
"Example of a model that gets used by other models"
Expand Down
30 changes: 30 additions & 0 deletions test_app/tests/rbac/api/test_rbac_validation.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import pytest
from django.test.utils import override_settings
from django.urls import reverse
from django.contrib.auth import get_user_model

from ansible_base.rbac.models import RoleDefinition
from ansible_base.lib.utils.auth import get_team_model

Team = get_team_model()
User = get_user_model()

@pytest.mark.django_db
class TestSharedAssignmentsDisabled:
Expand Down Expand Up @@ -86,3 +90,29 @@ def test_can_not_make_global_role_with_member_permission(admin_api_client):
)
assert response.status_code == 400
assert 'member_team permission can not be used in global roles' in str(response.data['permissions'])


@pytest.mark.django_db
def test_callback_validate_role_user_assignment(admin_api_client, inventory, inv_rd):
url = reverse('roleuserassignment-list')
user = User.objects.create(username='user-allowed')
response = admin_api_client.post(url, data={'object_id': inventory.id, 'user': user.id, 'role_definition': inv_rd.id})
assert response.status_code == 201

user = User.objects.create(username='user-not-allowed')
response = admin_api_client.post(url, data={'object_id': inventory.id, 'user': user.id, 'role_definition': inv_rd.id})
assert response.status_code == 403
assert "Role assignment not allowed" in response.data['detail']


@pytest.mark.django_db
def test_callback_validate_role_team_assignment(admin_api_client, inventory, organization, inv_rd):
url = reverse('roleteamassignment-list')
team = Team.objects.create(name='team-allowed', organization=organization)
response = admin_api_client.post(url, data={'object_id': inventory.id, 'team': team.id, 'role_definition': inv_rd.id})
assert response.status_code == 201

team = Team.objects.create(name='team-not-allowed', organization=organization)
response = admin_api_client.post(url, data={'object_id': inventory.id, 'team': team.id, 'role_definition': inv_rd.id})
assert response.status_code == 403
assert "Role assignment not allowed" in response.data['detail']

0 comments on commit d11602a

Please sign in to comment.