Skip to content

Commit f89ac79

Browse files
committed
Add tests for new content type RBAC model
1 parent e5e57eb commit f89ac79

File tree

6 files changed

+161
-9
lines changed

6 files changed

+161
-9
lines changed

ansible_base/rbac/management/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
def create_dab_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
1313
"""
1414
This is modified from the django auth.
15-
This will create DABPermission entries
15+
This will create DABPermission and DABContentType entries
1616
this will only create permissions for registered models
1717
"""
1818
if not getattr(app_config, 'models_module', None):
@@ -23,6 +23,7 @@ def create_dab_permissions(app_config, verbosity=2, interactive=True, using=DEFA
2323
if not any(cls._meta.app_label == app_label for cls in permission_registry._registry):
2424
return
2525

26+
# TODO: remove when migration is finished
2627
# Ensure that contenttypes are created for this app. Needed if
2728
# 'ansible_base.rbac' is in INSTALLED_APPS before
2829
# 'django.contrib.contenttypes'.
@@ -45,13 +46,21 @@ def create_dab_permissions(app_config, verbosity=2, interactive=True, using=DEFA
4546
if not router.allow_migrate_model(using, Permission):
4647
return
4748

49+
rbac_models = [klass for klass in app_config.get_models() if permission_registry.is_registered(klass)]
50+
51+
if not rbac_models:
52+
logger.debug(f'No RBAC models registered for app {app_label}')
53+
return
54+
55+
from .create_types import create_DAB_contenttypes
56+
57+
create_DAB_contenttypes(rbac_models, verbosity=verbosity, using=using, apps=apps)
58+
4859
# This will hold the permissions we're looking for as (content_type, (codename, name))
4960
searched_perms = []
5061
# The codenames and ctypes that should exist.
5162
ctypes = set()
52-
for klass in app_config.get_models():
53-
if not permission_registry.is_registered(klass):
54-
continue
63+
for klass in rbac_models:
5564
# Force looking up the content types in the current database
5665
# before creating foreign keys to them.
5766
ctype = ContentType.objects.db_manager(using).get_for_model(klass, for_concrete_model=False)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
from typing import Type
2+
3+
from django.apps import apps as global_apps
4+
from django.db import DEFAULT_DB_ALIAS, models
5+
6+
7+
def get_local_DAB_contenttypes(using: str, ct_model: Type[models.Model], service: str) -> dict[tuple[str, str], models.Model]:
8+
# This should work in migration scenarios, but other code checks for existence of it on manager
9+
ct_model.objects.clear_cache()
10+
11+
return {
12+
(service, ct.model): ct
13+
for ct in ct_model.objects.using(using).filter(service=service)
14+
}
15+
16+
17+
def create_DAB_contenttypes(
18+
rbac_models: list[Type[models.Model]],
19+
verbosity=2,
20+
using=DEFAULT_DB_ALIAS,
21+
apps=global_apps
22+
):
23+
"""Create DABContentType for models in the given app.
24+
25+
This is significantly different from the ContentType post-migrate method,
26+
because that creates types for all apps, and so this is only called one app at a time.
27+
DAB RBAC runs its post_migration logic just once, because the model list
28+
comes from the permission registry.
29+
"""
30+
DABContentType = apps.get_model("dab_rbac", "DABContentType")
31+
32+
from ansible_base.rbac.remote import get_local_resource_prefix
33+
34+
service = get_local_resource_prefix()
35+
36+
content_types = get_local_DAB_contenttypes(
37+
using, DABContentType, service
38+
)
39+
if not rbac_models:
40+
return
41+
42+
cts = [
43+
DABContentType(
44+
service=service,
45+
app_label=model._meta.app_label,
46+
model=model._meta.model_name,
47+
)
48+
for model in rbac_models
49+
if (service, model._meta.model_name) not in content_types
50+
]
51+
if not cts:
52+
return
53+
DABContentType.objects.using(using).bulk_create(cts)
54+
if verbosity >= 2:
55+
for ct in cts:
56+
print(
57+
"Adding DAB content type "
58+
f"'{ct.service}:{ct.app_label} | {ct.model}'"
59+
)

ansible_base/rbac/models/content_type.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from django.db.models.options import Options
77
from django.utils.translation import gettext_lazy as _
88

9-
from ..remote import get_remote_object_class, get_local_resource_prefix
9+
from ..remote import get_remote_object_class, get_local_resource_prefix, RemoteObject
1010

1111

1212
class DABContentTypeManager(django_models.Manager["DABContentType"]):
@@ -191,7 +191,7 @@ def model_class(self) -> Optional[Type[django_models.Model]]:
191191
except LookupError:
192192
return None
193193

194-
def get_object_for_this_type(self, **kwargs: Any) -> django_models.Model:
194+
def get_object_for_this_type(self, **kwargs: Any) -> django_models.Model | RemoteObject:
195195
"""Return the object referenced by this content type."""
196196
model = self.model_class()
197197
if model is None:
@@ -201,7 +201,7 @@ def get_object_for_this_type(self, **kwargs: Any) -> django_models.Model:
201201
return get_remote_object_class()(self, object_id)
202202
return model._base_manager.get(**kwargs)
203203

204-
def get_all_objects_for_this_type(self, **kwargs: Any) -> django_models.QuerySet | Sequence[django_models.Model]:
204+
def get_all_objects_for_this_type(self, **kwargs: Any) -> django_models.QuerySet | Sequence[django_models.Model | RemoteObject]:
205205
"""Return all objects referenced by this content type."""
206206
model = self.model_class()
207207
if model is None:

ansible_base/rbac/remote.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_local_resource_prefix() -> str:
3939
return 'local'
4040

4141

42-
def get_resource_prefix(cls: models.Model) -> str:
42+
def get_resource_prefix(cls: Type[models.Model]) -> str:
4343
"""The API project designator for given cls, according to the resource registry
4444
4545
This is used for related slug references, like "awx.inventory" to reference

ansible_base/rbac/validators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from ansible_base.lib.utils.models import is_add_perm
1010
from ansible_base.rbac.permission_registry import permission_registry
1111

12+
from .remote import get_resource_prefix
13+
1214

1315
def system_roles_enabled():
1416
return bool(
@@ -163,7 +165,7 @@ def validate_permissions_for_model(permissions, content_type: Optional[Model], m
163165
if content_type and perm.codename.startswith('view'):
164166
continue
165167
model = perm.content_type.model_class()
166-
if permission_registry.get_resource_prefix(model) == 'shared':
168+
if get_resource_prefix(model) == 'shared':
167169
raise ValidationError({'permissions', 'Local custom roles can only include view permission for shared models'})
168170

169171

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pytest
2+
from django.db import models
3+
from django.test import TestCase
4+
from django.test.utils import isolate_apps
5+
6+
from ansible_base.rbac.models import DABContentType
7+
from ansible_base.rbac.remote import RemoteObject
8+
9+
from test_app.models import Inventory
10+
11+
12+
@pytest.mark.django_db
13+
def test_post_migrate_creates_contenttype():
14+
ct = DABContentType.objects.get(app_label="test_app", model="inventory")
15+
assert ct.service == "aap"
16+
17+
18+
@pytest.mark.django_db
19+
class DABContentTypeTests(TestCase):
20+
"""These tests originally came from Django contenttypes"""
21+
def setUp(self):
22+
DABContentType.objects.clear_cache()
23+
self.addCleanup(DABContentType.objects.clear_cache)
24+
25+
def test_lookup_cache(self):
26+
with self.assertNumQueries(1):
27+
DABContentType.objects.get_for_model(Inventory)
28+
with self.assertNumQueries(0):
29+
ct = DABContentType.objects.get_for_model(Inventory)
30+
with self.assertNumQueries(0):
31+
DABContentType.objects.get_for_id(ct.id)
32+
with self.assertNumQueries(0):
33+
DABContentType.objects.get_by_natural_key(
34+
ct.service,
35+
ct.app_label,
36+
ct.model,
37+
)
38+
DABContentType.objects.clear_cache()
39+
with self.assertNumQueries(1):
40+
DABContentType.objects.get_for_model(Inventory)
41+
42+
@isolate_apps("tests")
43+
def test_get_for_model_create_contenttype(self):
44+
class ModelCreatedOnTheFly(models.Model):
45+
name = models.CharField(max_length=10)
46+
47+
class Meta:
48+
app_label = "tests"
49+
50+
ct = DABContentType.objects.get_for_model(ModelCreatedOnTheFly)
51+
assert ct.app_label == "tests"
52+
assert ct.model == "modelcreatedonthefly"
53+
54+
55+
@pytest.mark.django_db
56+
def test_get_object_for_this_type_remote():
57+
"""Remote objects should return a remote proxy."""
58+
ct = DABContentType.objects.create(
59+
service="remote_proj",
60+
app_label="testapp",
61+
model="book",
62+
)
63+
64+
obj = ct.get_object_for_this_type(pk=1)
65+
66+
assert isinstance(obj, RemoteObject)
67+
assert obj.object_id == 1
68+
assert obj.content_type == ct
69+
70+
71+
@pytest.mark.django_db
72+
def test_get_all_objects_for_this_type_remote():
73+
ct = DABContentType.objects.create(
74+
service="remote_proj2",
75+
app_label="testapp",
76+
model="book",
77+
)
78+
79+
objs = ct.get_all_objects_for_this_type(pk__in=[1, 2])
80+
81+
assert [o.object_id for o in objs] == [1, 2]
82+
assert all(isinstance(o, RemoteObject) for o in objs)

0 commit comments

Comments
 (0)