Skip to content

Commit dea634f

Browse files
authored
[AAP-54064] Decoupling apps from ansible_base.rbac (ansible#869)
Re-raising of ansible#849 This moves imports in-line, in cases where they would cause errors for projects that are using some apps, but not others
1 parent b344d21 commit dea634f

File tree

11 files changed

+388
-31
lines changed

11 files changed

+388
-31
lines changed

ansible_base/authentication/utils/claims.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
from ansible_base.authentication.models import Authenticator, AuthenticatorMap, AuthenticatorUser
2020
from ansible_base.authentication.utils.authenticator_map import check_role_type, expand_syntax
2121
from ansible_base.lib.abstract_models import AbstractOrganization, AbstractTeam, CommonModel
22+
from ansible_base.lib.utils.apps import is_rbac_installed
2223
from ansible_base.lib.utils.auth import get_organization_model, get_team_model
2324
from ansible_base.lib.utils.string import is_empty
24-
from ansible_base.rbac.models import DABContentType
25-
from ansible_base.rbac.remote import get_local_resource_prefix
2625

2726
from .trigger_definition import TRIGGER_DEFINITION
2827

@@ -722,7 +721,7 @@ def reconcile_user_claims(cls, user: AbstractUser, authenticator_user: Authentic
722721

723722
claims = getattr(user, 'claims', authenticator_user.claims)
724723

725-
if 'ansible_base.rbac' in settings.INSTALLED_APPS:
724+
if is_rbac_installed():
726725
cls(claims, user, authenticator_user).manage_permissions()
727726
else:
728727
logger.info(_("Skipping user claims with RBAC roles, because RBAC app is not installed"))
@@ -876,7 +875,11 @@ class RoleUserAssignmentsCache:
876875
def __init__(self):
877876
self.cache = {}
878877
# NOTE(cutwater): We may probably execute this query once and cache the query results.
879-
self.content_types = {content_type.model: content_type for content_type in DABContentType.objects.get_for_models(Organization, Team).values()}
878+
self.content_types = {}
879+
if is_rbac_installed():
880+
from ansible_base.rbac.models import DABContentType
881+
882+
self.content_types = {content_type.model: content_type for content_type in DABContentType.objects.get_for_models(Organization, Team).values()}
880883
self.role_definitions = {}
881884

882885
def items(self):
@@ -956,6 +959,11 @@ def cache_existing(self, role_assignments: Iterable[models.Model]) -> None:
956959
- All cached assignments are marked with STATUS_EXISTING status
957960
- Role definitions are also cached separately in self.role_definitions
958961
"""
962+
local_resource_prefixes = ["shared"]
963+
from ansible_base.rbac.remote import get_local_resource_prefix # RBAC must be installed to use method
964+
965+
local_resource_prefixes.append(get_local_resource_prefix())
966+
959967
for role_assignment in role_assignments:
960968
# Cache role definition
961969
if (role_definition := self._rd_by_id(role_assignment)) is None:
@@ -965,7 +973,7 @@ def cache_existing(self, role_assignments: Iterable[models.Model]) -> None:
965973
# Skip role assignments that should not be cached
966974
if not (
967975
role_assignment.content_type is None # Global/system roles (e.g., System Auditor)
968-
or role_assignment.content_type.service in [get_local_resource_prefix(), "shared"]
976+
or role_assignment.content_type.service in local_resource_prefixes
969977
): # Local object roles
970978
continue
971979

ansible_base/lib/routers/association_resource_router.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from itertools import chain
44
from typing import Type
55

6-
from django.conf import settings
76
from django.db.models import Model
87
from django.db.models.fields import IntegerField
98
from django.db.models.query import QuerySet
@@ -17,7 +16,7 @@
1716
from rest_framework.response import Response
1817
from rest_framework.viewsets import ViewSetMixin
1918

20-
from ansible_base.rbac.permission_registry import permission_registry
19+
from ansible_base.lib.utils.apps import is_rbac_installed
2120

2221
logger = logging.getLogger('ansible_base.lib.routers.association_resource_router')
2322

@@ -119,10 +118,13 @@ def check_parent_object_permissions(self, request, parent_obj: Model) -> None:
119118
will not check "change" permissions to the parent object on POST
120119
this method checks parent change permission, view permission should be handled by filter_queryset
121120
"""
122-
if (request.method not in SAFE_METHODS) and 'ansible_base.rbac' in settings.INSTALLED_APPS and permission_registry.is_registered(parent_obj):
123-
from ansible_base.rbac.policies import check_content_obj_permission
121+
if (request.method not in SAFE_METHODS) and is_rbac_installed():
122+
from ansible_base.rbac.permission_registry import permission_registry
124123

125-
check_content_obj_permission(request.user, parent_obj)
124+
if permission_registry.is_registered(parent_obj):
125+
from ansible_base.rbac.policies import check_content_obj_permission
126+
127+
check_content_obj_permission(request.user, parent_obj)
126128

127129
def get_parent_object(self) -> Model:
128130
"""Modeled mostly after DRF get_object, but for the parent model

ansible_base/lib/testing/util.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ def delete_authenticator(authenticator):
4747
class StaticResourceAPIClient(ResourceAPIClient):
4848
"""A testing API client that reads response router attribute or static files."""
4949

50-
router = {}
51-
# Route is used to force a certain status,response for a route
52-
# It has to be a mutable default but the fixture instantiates one for
53-
# each test.
50+
def __init__(self, *args, **kwargs):
51+
# Route is used to force a certain status,response for a route
52+
# It has to be a mutable default but the fixture instantiates one for
53+
# each test.
54+
self.router = {}
55+
super().__init__(*args, **kwargs)
5456

5557
def _make_request(self, method, path, data=None, params=None, stream=False):
5658
response = Response()

ansible_base/lib/utils/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def is_rbac_installed() -> bool:
2+
from django.conf import settings
3+
4+
return bool('ansible_base.rbac' in settings.INSTALLED_APPS)

ansible_base/resource_registry/registry.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from django.contrib.auth import authenticate
55
from django.utils.translation import gettext_lazy as _
66

7+
from ansible_base.lib.utils.apps import is_rbac_installed
78
from ansible_base.resource_registry.utils.resource_type_processor import ResourceTypeProcessor, RoleDefinitionProcessor
89

910
ParentResource = namedtuple("ParentResource", ["model", "field_name"])
@@ -23,12 +24,16 @@ class ServiceAPIConfig:
2324
This will be the interface for configuring the resource registry for each service.
2425
"""
2526

26-
_default_resource_processors = {
27-
"shared.team": ResourceTypeProcessor,
28-
"shared.organization": ResourceTypeProcessor,
29-
"shared.user": ResourceTypeProcessor,
30-
"shared.roledefinition": RoleDefinitionProcessor,
31-
}
27+
@classmethod
28+
def _get_default_resource_processors(cls):
29+
processors = {
30+
"shared.team": ResourceTypeProcessor,
31+
"shared.organization": ResourceTypeProcessor,
32+
"shared.user": ResourceTypeProcessor,
33+
}
34+
if is_rbac_installed():
35+
processors["shared.roledefinition"] = RoleDefinitionProcessor
36+
return processors
3237

3338
custom_resource_processors = {}
3439

@@ -43,7 +48,7 @@ def authenticate_local_user(username: str, password: str):
4348

4449
@classmethod
4550
def get_processor(cls, resource_type):
46-
combined_processors = {**cls._default_resource_processors, **cls.custom_resource_processors}
51+
combined_processors = {**cls._get_default_resource_processors(), **cls.custom_resource_processors}
4752
return combined_processors[resource_type]
4853

4954

ansible_base/resource_registry/rest_client.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77
import urllib3
88
from django.apps import apps
99

10+
from ansible_base.lib.utils.apps import is_rbac_installed
1011
from ansible_base.resource_registry.resource_server import get_resource_server_config, get_service_token
1112

13+
14+
def _check_rbac_installed():
15+
"""Check if ansible_base.rbac is installed and raise RuntimeError if not."""
16+
if not is_rbac_installed():
17+
raise RuntimeError("This operation requires ansible_base.rbac to be installed")
18+
19+
1220
ResourceRequestBody = namedtuple(
1321
"ResourceRequestBody",
1422
["ansible_id", "service_id", "is_partially_migrated", "resource_type", "resource_data"],
@@ -186,6 +194,7 @@ def list_team_assignments(self, team_ansible_id: Optional[str] = None, filters:
186194
return self._make_request("get", "role-team-assignments/", params=params)
187195

188196
def sync_assignment(self, assignment):
197+
_check_rbac_installed()
189198
from ansible_base.rbac.service_api.serializers import ServiceRoleTeamAssignmentSerializer, ServiceRoleUserAssignmentSerializer
190199

191200
if assignment._meta.model_name == 'roleuserassignment':
@@ -196,6 +205,7 @@ def sync_assignment(self, assignment):
196205
return self._sync_assignment(serializer.data)
197206

198207
def sync_unassignment(self, role_definition, actor, content_object):
208+
_check_rbac_installed()
199209
data = {'role_definition': role_definition.name}
200210
data[f'{actor._meta.model_name}_ansible_id'] = str(actor.resource.ansible_id)
201211

@@ -214,6 +224,7 @@ def sync_unassignment(self, role_definition, actor, content_object):
214224

215225
def sync_object_deletion(self, content_object):
216226
"""Sync object deletion to Gateway for cleanup of all related role assignments"""
227+
_check_rbac_installed()
217228
from ansible_base.rbac.models import DABContentType
218229

219230
# Get the content type information

ansible_base/resource_registry/shared_types.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from rest_framework import serializers
22
from rest_framework.exceptions import ValidationError
33

4-
from ansible_base.rbac.models import DABContentType, DABPermission
4+
from ansible_base.lib.utils.apps import is_rbac_installed
55
from ansible_base.resource_registry.utils.resource_type_serializers import AnsibleResourceForeignKeyField, SharedResourceTypeSerializer
66
from ansible_base.resource_registry.utils.sso_provider import get_sso_provider_server
77

@@ -84,6 +84,8 @@ class LenientPermissionSlugListField(serializers.ListField):
8484
child = serializers.CharField()
8585

8686
def to_internal_value(self, data):
87+
from ansible_base.rbac.models import DABPermission
88+
8789
data = super().to_internal_value(data)
8890
return list(DABPermission.objects.filter(api_slug__in=data))
8991

@@ -98,14 +100,24 @@ class RoleDefinitionType(SharedResourceTypeSerializer):
98100
name = serializers.CharField()
99101
description = serializers.CharField(default="", allow_blank=True)
100102
managed = serializers.BooleanField()
101-
content_type = serializers.SlugRelatedField(
102-
slug_field='api_slug',
103-
queryset=DABContentType.objects.all(),
104-
allow_null=True,
105-
default=None,
106-
)
107103
permissions = LenientPermissionSlugListField()
108104

105+
def __init__(self, *args, **kwargs):
106+
if not is_rbac_installed():
107+
raise RuntimeError("RoleDefinitionType requires ansible_base.rbac to be installed")
108+
109+
super().__init__(*args, **kwargs)
110+
111+
# Set up content_type field only when rbac is available
112+
from ansible_base.rbac.models import DABContentType
113+
114+
self.fields['content_type'] = serializers.SlugRelatedField(
115+
slug_field='api_slug',
116+
queryset=DABContentType.objects.all(),
117+
allow_null=True,
118+
default=None,
119+
)
120+
109121
def is_valid(self, raise_exception=False):
110122
try:
111123
return super().is_valid(raise_exception=raise_exception)

ansible_base/resource_registry/tasks/sync.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from django.db.utils import Error, IntegrityError
1919
from requests import HTTPError
2020

21-
from ansible_base.rbac.models.role import AssignmentBase, RoleDefinition, RoleTeamAssignment, RoleUserAssignment
21+
from ansible_base.lib.utils.apps import is_rbac_installed
2222
from ansible_base.resource_registry.models import Resource, ResourceType
2323
from ansible_base.resource_registry.models.service_identifier import service_id
2424
from ansible_base.resource_registry.registry import get_registry
@@ -139,7 +139,9 @@ def fetch_manifest(
139139
return [ManifestItem(**row) for row in csv_reader]
140140

141141

142-
def get_ansible_id_or_pk(assignment: AssignmentBase) -> str:
142+
def get_ansible_id_or_pk(assignment) -> str:
143+
if not is_rbac_installed():
144+
raise RuntimeError("get_ansible_id_or_pk requires ansible_base.rbac to be installed")
143145
# For object-scoped assignments, try to get the object's ansible_id
144146
if assignment.content_type.model in ('organization', 'team'):
145147
object_resource = Resource.objects.filter(object_id=assignment.object_id, content_type__model=assignment.content_type.model).first()
@@ -153,7 +155,9 @@ def get_ansible_id_or_pk(assignment: AssignmentBase) -> str:
153155
return str(ansible_id_or_pk)
154156

155157

156-
def get_content_object(role_definition: RoleDefinition, assignment_tuple: AssignmentTuple) -> Any:
158+
def get_content_object(role_definition, assignment_tuple: AssignmentTuple) -> Any:
159+
if not is_rbac_installed():
160+
raise RuntimeError("get_content_object requires ansible_base.rbac to be installed")
157161
content_object = None
158162
if role_definition.content_type.model in ('organization', 'team'):
159163
object_resource = Resource.objects.get(ansible_id=assignment_tuple.ansible_id_or_pk)
@@ -238,6 +242,10 @@ def get_remote_assignments(api_client: ResourceAPIClient) -> set[AssignmentTuple
238242

239243
def get_local_assignments() -> set[AssignmentTuple]:
240244
"""Get local assignments and convert to tuples."""
245+
if not is_rbac_installed():
246+
raise RuntimeError("get_local_assignments requires ansible_base.rbac to be installed")
247+
from ansible_base.rbac.models.role import RoleTeamAssignment, RoleUserAssignment
248+
241249
assignments = set()
242250

243251
# Get user assignments
@@ -294,6 +302,10 @@ def get_local_assignments() -> set[AssignmentTuple]:
294302

295303
def delete_local_assignment(assignment_tuple: AssignmentTuple) -> bool:
296304
"""Delete a local assignment based on the tuple."""
305+
if not is_rbac_installed():
306+
raise RuntimeError("delete_local_assignment requires ansible_base.rbac to be installed")
307+
from ansible_base.rbac.models.role import RoleDefinition
308+
297309
try:
298310
role_definition = RoleDefinition.objects.get(name=assignment_tuple.role_definition_name)
299311

@@ -320,6 +332,10 @@ def delete_local_assignment(assignment_tuple: AssignmentTuple) -> bool:
320332

321333
def create_local_assignment(assignment_tuple: AssignmentTuple) -> bool:
322334
"""Create a local assignment based on the tuple."""
335+
if not is_rbac_installed():
336+
raise RuntimeError("create_local_assignment requires ansible_base.rbac to be installed")
337+
from ansible_base.rbac.models.role import RoleDefinition
338+
323339
try:
324340
role_definition = RoleDefinition.objects.get(name=assignment_tuple.role_definition_name)
325341

@@ -694,6 +710,10 @@ def _sync_assignments(self):
694710
if not self.sync_assignments:
695711
return
696712

713+
if not is_rbac_installed():
714+
self.write(">>> Skipping role assignments sync (rbac not installed)")
715+
return
716+
697717
self.write(">>> Syncing role assignments")
698718

699719
try:

0 commit comments

Comments
 (0)