Skip to content

Commit

Permalink
Merge pull request #27 from jaredlewis/develop
Browse files Browse the repository at this point in the history
Updates to registration logic, ability to add custom PermSet
  • Loading branch information
jaredlewis committed Feb 21, 2022
2 parents 993b071 + e0df95c commit a20219f
Show file tree
Hide file tree
Showing 12 changed files with 635 additions and 347 deletions.
6 changes: 6 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
Release Notes
=============

v2.0.0
------
* Removed `RESTRAINT_CONFIG` global variable, replaced with `get_restraint_config` method
* Removed `register_restraint_config` method, replaced with `RESTRAINT_CONFIGURATION` django setting
* Added `is_private` flag to PermSet model

v1.2.0
------
* Python 3.7
Expand Down
93 changes: 47 additions & 46 deletions docs/setup.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,66 @@ Setup

The Restraint Configuration
---------------------------
Restraint is configured all in one place using :code:`restraint.register_restraint_config`. This function must be called during load time in order for Restraint to be able to perform dynamic permission queries and object-level filtering.
Restraint is configured all in one place using the :code:`RESTRAINT_CONFIGURATION` django setting.
This should be a string path to a method that will return the fully qualified restraint config.

An example Restraint configuration is provided below. Details of the configuration are outlined in later sections.


.. code-block:: python
from restraint import register_restraint_config
RESTRAINT_CONFIGURATION = 'app.permissions.get_restraint_config'
register_restraint_config({
'perm_set_getter': perm_set_getter_function,
'perm_sets': {
'super': {
'display_name': 'Super',
},
'individual': {
'display_name': 'Individual',
def get_restraint_config():
return {
'perm_set_getter': perm_set_getter_function,
'perm_sets': {
'super': {
'display_name': 'Super',
},
'individual': {
'display_name': 'Individual',
},
'staff': {
'display_name': 'Staff'
}
},
'staff': {
'display_name': 'Staff'
}
},
'perms': {
'can_edit_stuff': {
'display_name': 'Can Edit Stuff',
'levels': {
'all_stuff': {
'display_name': 'All Stuff',
'id_filter': None,
},
'some_stuff': {
'display_name': 'Some Stuff',
'id_filter': lambda a: User.objects.filter(id=a.id).values_list('id', flat=True),
},
'only_superusers': {
'display_name': 'Only Superusers',
'id_filter': lambda a: User.objects.filter(is_superuser=True).values_list('id', flat=True),
'perms': {
'can_edit_stuff': {
'display_name': 'Can Edit Stuff',
'levels': {
'all_stuff': {
'display_name': 'All Stuff',
'id_filter': None,
},
'some_stuff': {
'display_name': 'Some Stuff',
'id_filter': lambda a: User.objects.filter(id=a.id).values_list('id', flat=True),
},
'only_superusers': {
'display_name': 'Only Superusers',
'id_filter': lambda a: User.objects.filter(is_superuser=True).values_list('id', flat=True),
},
},
},
'can_view_stuff': {
'display_name': 'Can View Stuff',
'levels': constants.BOOLEAN_LEVELS_CONFIG,
}
},
'can_view_stuff': {
'display_name': 'Can View Stuff',
'levels': constants.BOOLEAN_LEVELS_CONFIG,
}
},
'default_access': {
'super': {
'can_edit_stuff': ['all_stuff', 'some_stuff'],
'can_view_stuff': [constants.BOOLEAN_LEVELS_NAME],
},
'individual': {
'can_edit_stuff': ['some_stuff'],
},
'staff': {
'can_edit_stuff': ['some_stuff', 'only_superusers']
'default_access': {
'super': {
'can_edit_stuff': ['all_stuff', 'some_stuff'],
'can_view_stuff': [constants.BOOLEAN_LEVELS_NAME],
},
'individual': {
'can_edit_stuff': ['some_stuff'],
},
'staff': {
'can_edit_stuff': ['some_stuff', 'only_superusers']
}
}
}
})
Defining The Permission Set Getter
----------------------------------
Expand Down
24 changes: 7 additions & 17 deletions restraint/core.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
from collections import defaultdict
from itertools import chain

from django.conf import settings
from django.db.models import Q
from django.utils.module_loading import import_string

from restraint import models


# A global variable for holding the configuration of django restraint
RESTRAINT_CONFIG = {}


def register_restraint_config(restraint_config):
RESTRAINT_CONFIG.clear()
RESTRAINT_CONFIG.update(restraint_config)


def get_restraint_config():
if RESTRAINT_CONFIG:
return RESTRAINT_CONFIG
else:
raise RuntimeError('No restraint config has been registered')
return import_string(settings.RESTRAINT_CONFIGURATION)()


def update_restraint_db(flush_default_access=False):
Expand Down Expand Up @@ -70,9 +60,9 @@ def _load_perms(self, account, which_perms):
perm_levels = perm_levels.filter(perm__name__in=which_perms)

self._perms = defaultdict(dict)
for l in perm_levels:
self._perms[l.perm.name].update({
l.name: self._config['perms'][l.perm.name]['levels'][l.name]['id_filter']
for level in perm_levels:
self._perms[level.perm.name].update({
level.name: self._config['perms'][level.perm.name]['levels'][level.name]['id_filter']
})

def has_perm(self, perm, level=None):
Expand Down Expand Up @@ -110,4 +100,4 @@ def filter_qset(self, qset, perm, restrict_kwargs=None):
return qset
else:
# Filter the queryset by the union of all filters
return qset.filter(id__in=set(chain(*[l(self._user) for l in self._perms[perm].values()])))
return qset.filter(id__in=set(chain(*[level(self._user) for level in self._perms[perm].values()])))
170 changes: 170 additions & 0 deletions restraint/managers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from django.contrib.contenttypes.models import ContentType
from django.db import models
from manager_utils import sync


class PermSetManager(models.Manager):
def sync_perm_sets(self, perm_sets):
"""
Syncs all private perm sets the provided dictionary of perm sets to PermSet models.
"""
from restraint.models import PermSet
sync(
queryset=self.get_queryset().filter(
is_private=True
),
model_objs=[
PermSet(
name=name,
display_name=config.get('display_name', ''),
is_private=True
)
for name, config in perm_sets.items()
],
unique_fields=[
'name'
],
update_fields=[
'display_name'
]
)


class PermManager(models.Manager):
def sync_perms(self, perms):
"""
Syncs the perms to Perm models.
"""
from restraint.models import Perm
return sync(
queryset=self.get_queryset(),
model_objs=[
Perm(
name=name,
display_name=config.get('display_name', '')
)
for name, config in perms.items()
],
unique_fields=[
'name'
],
update_fields=[
'display_name'
],
return_upserts_distinct=True
)


class PermLevelManager(models.Manager):
def sync_perm_levels(self, perms):
"""
Given a dictionary of perms that map to perm levels, sync the perm levels
to PermLevel objects in the database.
"""
from restraint.models import Perm, PermLevel
perm_objs = {
p.name: p
for p in Perm.objects.all()
}
perm_levels = []
for perm, perm_config in perms.items():
assert(perm_config['levels'])
for level, level_config in perm_config['levels'].items():
perm_levels.append(PermLevel(
perm=perm_objs[perm],
name=level,
display_name=level_config.get('display_name', '')
))
sync(
queryset=self.get_queryset(),
model_objs=perm_levels,
unique_fields=[
'name',
'perm'
],
update_fields=[
'display_name'
]
)


class PermAccessManager(models.Manager):
def set_default(self, permission_set_name, permission_name, levels=None):
"""
Sets default levels for a permission for a permission set
:param permission_set_name: The name of the permission set
:param permission_name: The name of the permission
:param levels: A list of levels
"""
from restraint.models import PermSet, PermAccess, PermLevel
permission_access = PermAccess.objects.get_or_create(
perm_set=PermSet.objects.get(name=permission_set_name)
)[0]
if levels:
permission_levels = PermLevel.objects.filter(
perm__name=permission_name,
name__in=levels
)
permission_access.perm_levels.set(permission_levels)
else:
permission_access.perm_levels.clear()

def update_perm_set_access(self, config, new_perms=None, flush_previous_config=False):
"""
Update the access for private perm sets with a config. The user can optionally flush
the previous config and set it to the new one.
"""

# Do model imports to avoid circular
from restraint.models import PermSet, PermAccess, PermLevel

# Ensure that new perms is not none
if new_perms is None: # pragma: no cover
new_perms = []

# Loop over each private permission set
for perm_set in PermSet.objects.filter(is_private=True):
perm_access, created = PermAccess.objects.get_or_create(perm_set=perm_set)
perm_access_levels = []
for perm, perm_levels in config.get(perm_set.name, {}).items():
# If we are not flushing the previous config, continue if the perm not among the newly created perms
# this is necessary because perm access is mutable; We don't want to destroy modifications made to
# existing permissions
if not created and not flush_previous_config and perm not in [p.name for p in new_perms]:
continue
assert(perm_levels)
perm_access_levels.extend(PermLevel.objects.filter(perm__name=perm, name__in=perm_levels))

if flush_previous_config:
perm_access.perm_levels.clear()
perm_access.perm_levels.add(*perm_access_levels)

def add_individual_access(self, user, perm_name, level_name):
"""
Given a user, a permission name, and the name of the level, add the level in the permission access for
the individual user.
"""
from restraint.models import PermAccess, PermLevel
pa, created = PermAccess.objects.get_or_create(
perm_user_id=user.id,
perm_user_type=ContentType.objects.get_for_model(user)
)
pa.perm_levels.add(PermLevel.objects.get(
perm__name=perm_name,
name=level_name
))

def remove_individual_access(self, user, perm_name, level_name):
"""
Given a user, a permission name, and the name of the level, remove the level in the permission access for
the individual user.
"""
from restraint.models import PermAccess, PermLevel
pa = PermAccess.objects.get(
perm_user_id=user.id,
perm_user_type=ContentType.objects.get_for_model(user)
)
pa.perm_levels.remove(PermLevel.objects.get(
perm__name=perm_name,
name=level_name
))
26 changes: 26 additions & 0 deletions restraint/migrations/0002_permset_is_private.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 3.2.12 on 2022-02-20 21:32

from django.db import migrations, models


def update_private_permission_sets(apps, schema_editor):
PermSet = apps.get_model('restraint', 'PermSet')
for perm_set in PermSet.objects.all():
perm_set.is_private = True
perm_set.save(update_fields=['is_private'])


class Migration(migrations.Migration):

dependencies = [
('restraint', '0001_initial'),
]

operations = [
migrations.AddField(
model_name='permset',
name='is_private',
field=models.BooleanField(default=False),
),
migrations.RunPython(update_private_permission_sets)
]

0 comments on commit a20219f

Please sign in to comment.