Skip to content

Commit

Permalink
Allow disabling object-level roles
Browse files Browse the repository at this point in the history
  • Loading branch information
AlanCoding committed Jun 20, 2024
1 parent 9e7ba7f commit 3a52d4a
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 36 deletions.
4 changes: 4 additions & 0 deletions ansible_base/rbac/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ def get(self, request, format=None):
if cls is None:
cls_repr = 'system'
else:
info = permission_registry.get_info(cls)
if not info.allow_object_roles:
continue
cls_repr = f"{permission_registry.get_resource_prefix(cls)}.{cls._meta.model_name}"

allowed_permissions[cls_repr] = []
for codename in list_combine_values(permissions_allowed_for_role(cls)):
perm = permission_registry.permission_qs.get(codename=codename)
Expand Down
2 changes: 1 addition & 1 deletion ansible_base/rbac/management/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def create_dab_permissions(app_config, verbosity=2, interactive=True, using=DEFA

# exit early if nothing is registered for this app
app_label = app_config.label
if not any(cls._meta.app_label == app_label for cls in permission_registry._registry):
if not any(info.app_label == app_label for info in permission_registry._registry.values()):
return

# Ensure that contenttypes are created for this app. Needed if
Expand Down
82 changes: 48 additions & 34 deletions ansible_base/rbac/permission_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,43 +22,58 @@
logger = logging.getLogger('ansible_base.rbac.permission_registry')


class ModelPermissionInfo:
"""Container of RBAC registration information for a model in permission_registry"""
def __init__(self, model, parent_field_name='organization', allow_object_roles=True):
self.model_name = model._meta.model_name
self.app_label = model._meta.app_label
if parent_field_name == self.model_name:
# model can not be its own parent
self.parent_field_name = None
else:
self.parent_field_name = parent_field_name
self.allow_object_roles = allow_object_roles
self.model = model


class PermissionRegistry:
def __init__(self):
self._registry = set() # model registry
self._name_to_model = dict()
self._parent_fields = dict()
self._registry = dict() # model registry
self._managed_roles = dict() # code-defined role definitions, managed=True
self.apps_ready = False
self._tracked_relationships = set()
self._trackers = dict()

def register(self, *args, parent_field_name='organization'):
def register(self, *args, **kwargs):
if self.apps_ready:
raise RuntimeError('Cannot register model to permission_registry after apps are ready')
for cls in args:
if cls not in self._registry:
self._registry.add(cls)
model_name = cls._meta.model_name
if model_name in self._name_to_model:
raise RuntimeError(f'Two models registered with same name {model_name}')
self._name_to_model[model_name] = cls
if model_name != 'organization':
self._parent_fields[model_name] = parent_field_name
for model in args:
if model._meta.model_name not in self._registry:
info = ModelPermissionInfo(model, **kwargs)
self._registry[info.model_name] = info
elif self._registry[model._meta.model_name] is model:
logger.debug(f'Model {model._meta.model_name} registered to permission registry more than once')
else:
logger.debug(f'Model {cls._meta.model_name} registered to permission registry more than once')
raise RuntimeError(f'Two models registered with same name {model._meta.model_name}')

def get_info(self, obj: Union[ModelBase, Model]) -> ModelPermissionInfo:
return self._registry[obj._meta.model_name]

def track_relationship(self, cls, relationship, role_name):
self._tracked_relationships.add((cls, relationship, role_name))

def get_parent_model(self, model) -> Optional[type]:
model = self._name_to_model[model._meta.model_name]
parent_field_name = self.get_parent_fd_name(model)
if parent_field_name is None:
info = self._registry[model._meta.model_name]
if info.parent_field_name is None:
return None
return model._meta.get_field(parent_field_name).related_model
return model._meta.get_field(info.parent_field_name).related_model

def get_parent_fd_name(self, model) -> Optional[str]:
return self._parent_fields.get(model._meta.model_name)
model_name = model._meta.model_name
if model_name not in self._registry:
return None
info = self._registry[model_name]
return info.parent_field_name

def get_child_models(self, parent_model, seen=None) -> list[tuple[str, Type[Model]]]:
"""Returns child models and the filter relationship to the parent
Expand All @@ -72,19 +87,18 @@ def get_child_models(self, parent_model, seen=None) -> list[tuple[str, Type[Mode
seen = set()
child_filters = []
parent_model_name = parent_model._meta.model_name
for model_name, parent_field_name in self._parent_fields.items():
if parent_field_name is None:
for model_name, info in self._registry.items():
if info.parent_field_name is None:
continue
child_model = self._name_to_model[model_name]
this_parent_name = child_model._meta.get_field(parent_field_name).related_model._meta.model_name
this_parent_name = info.model._meta.get_field(info.parent_field_name).related_model._meta.model_name
if this_parent_name == parent_model_name:
if model_name in seen:
continue
seen.add(model_name)

child_filters.append((parent_field_name, child_model))
for next_parent_filter, grandchild_model in self.get_child_models(child_model, seen=seen):
child_filters.append((f'{next_parent_filter}__{parent_field_name}', grandchild_model))
child_filters.append((info.parent_field_name, info.model))
for next_parent_filter, grandchild_model in self.get_child_models(info.model, seen=seen):
child_filters.append((f'{next_parent_filter}__{info.parent_field_name}', grandchild_model))
return child_filters

def get_resource_prefix(self, cls: Type[Model]) -> str:
Expand Down Expand Up @@ -150,8 +164,8 @@ def call_when_apps_ready(self, apps, app_config):
self.apps = apps
self.apps_ready = True

if self.team_model not in self._registry:
self._registry.add(self.team_model)
if self.team_model._meta.model_name not in self._registry:
self.register(self.team_model)

# Do no specify sender for create_dab_permissions, because that is passed as app_config
# and we want to create permissions for external apps, not the dab_rbac app
Expand All @@ -169,9 +183,9 @@ def call_when_apps_ready(self, apps, app_config):
self.user_model.add_to_class('singleton_permissions', bound_singleton_permissions)
post_delete.connect(triggers.rbac_post_user_delete, sender=self.user_model, dispatch_uid='permission-registry-user-delete')

for cls in self._registry:
triggers.connect_rbac_signals(cls)
connect_rbac_methods(cls)
for cls in self._registry.values():
triggers.connect_rbac_signals(cls.model)
connect_rbac_methods(cls.model)

for cls, relationship, role_name in self._tracked_relationships:
if role_name in self._trackers:
Expand Down Expand Up @@ -221,12 +235,12 @@ def team_permission(self):
return f'member_{self.team_model._meta.model_name}'

@property
def all_registered_models(self):
return list(self._registry)
def all_registered_models(self) -> list[Type[Model]]:
return [info.model for info in self._registry.values()]

def is_registered(self, obj: Union[ModelBase, Model]) -> bool:
"""Tells if the given object or class is a type tracked by DAB RBAC"""
return any(obj._meta.model_name == cls._meta.model_name for cls in self._registry)
return bool(obj._meta.model_name in self._registry)


permission_registry = PermissionRegistry()
15 changes: 14 additions & 1 deletion docs/apps/rbac/for_app_developers.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ Removing permission will delete the object role if no other assignments exist.
Any Django Model (except your user model) can
be made into a resource in the RBAC system by registering that resource in the registry.

```
```python
from ansible_base.rbac.permission_registry import permission_registry

permission_registry.register(MyModel, parent_field_name='organization')
Expand Down Expand Up @@ -133,6 +133,19 @@ then no other _registered_ model should list that permission.
This rules out things like registering a model, and also registering a proxy
model _of that model_.

#### Disabling Object-level Permissions

You can specifically disable giving object-level permissions at registration of a model.

```python
permission_registry.register(MyModel, allow_object_roles=False)
```

This will not allow creating any role_definitions with a `content_type` for that model.
Users will still be able to get permission to `MyModel` objects by either
obtaining an organization-level (or other parent object) role, or by a system-level role.
This just prevents using object-level roles through the API.

### Parent Resources

Assuming `obj` has a related `organization` which was declared by `parent_field_name` when registering,
Expand Down

0 comments on commit 3a52d4a

Please sign in to comment.