mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1018 from SchrodingersGat/group-roles
Roles and Permissions
This commit is contained in:
commit
dc41231fcc
@ -138,6 +138,7 @@ INSTALLED_APPS = [
|
|||||||
'part.apps.PartConfig',
|
'part.apps.PartConfig',
|
||||||
'report.apps.ReportConfig',
|
'report.apps.ReportConfig',
|
||||||
'stock.apps.StockConfig',
|
'stock.apps.StockConfig',
|
||||||
|
'users.apps.UsersConfig',
|
||||||
|
|
||||||
# Third part add-ons
|
# Third part add-ons
|
||||||
'django_filters', # Extended filter functionality
|
'django_filters', # Extended filter functionality
|
||||||
|
@ -38,4 +38,5 @@ class CompanyConfig(AppConfig):
|
|||||||
company.image = None
|
company.image = None
|
||||||
company.save()
|
company.save()
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
print("Could not generate Company thumbnails")
|
# Getting here probably meant the database was in test mode
|
||||||
|
pass
|
||||||
|
@ -24,7 +24,6 @@ def reverse_association(apps, schema_editor):
|
|||||||
# Exit if there are no SupplierPart objects
|
# Exit if there are no SupplierPart objects
|
||||||
# This crucial otherwise the unit test suite fails!
|
# This crucial otherwise the unit test suite fails!
|
||||||
if SupplierPart.objects.count() == 0:
|
if SupplierPart.objects.count() == 0:
|
||||||
print("No SupplierPart objects - skipping")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
print("Reversing migration for manufacturer association")
|
print("Reversing migration for manufacturer association")
|
||||||
@ -105,7 +104,6 @@ def associate_manufacturers(apps, schema_editor):
|
|||||||
# Exit if there are no SupplierPart objects
|
# Exit if there are no SupplierPart objects
|
||||||
# This crucial otherwise the unit test suite fails!
|
# This crucial otherwise the unit test suite fails!
|
||||||
if SupplierPart.objects.count() == 0:
|
if SupplierPart.objects.count() == 0:
|
||||||
print("No SupplierPart objects - skipping")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Link a 'manufacturer_name' to a 'Company'
|
# Link a 'manufacturer_name' to a 'Company'
|
||||||
|
@ -37,4 +37,4 @@ class PartConfig(AppConfig):
|
|||||||
part.image = None
|
part.image = None
|
||||||
part.save()
|
part.save()
|
||||||
except (OperationalError, ProgrammingError):
|
except (OperationalError, ProgrammingError):
|
||||||
print("Could not generate Part thumbnails")
|
pass
|
||||||
|
@ -1,3 +1,135 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
# from django.contrib import admin
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
|
||||||
|
from users.models import RuleSet
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
|
class RuleSetInline(admin.TabularInline):
|
||||||
|
"""
|
||||||
|
Class for displaying inline RuleSet data in the Group admin page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
model = RuleSet
|
||||||
|
can_delete = False
|
||||||
|
verbose_name = 'Ruleset'
|
||||||
|
verbose_plural_name = 'Rulesets'
|
||||||
|
fields = ['name'] + [option for option in RuleSet.RULE_OPTIONS]
|
||||||
|
readonly_fields = ['name']
|
||||||
|
max_num = len(RuleSet.RULESET_CHOICES)
|
||||||
|
min_num = 1
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeGroupAdminForm(forms.ModelForm):
|
||||||
|
"""
|
||||||
|
Custom admin form for the Group model.
|
||||||
|
|
||||||
|
Adds the ability for editing user membership directly in the group admin page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Group
|
||||||
|
exclude = []
|
||||||
|
fields = [
|
||||||
|
'name',
|
||||||
|
'users',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
if self.instance.pk:
|
||||||
|
# Populate the users field with the current Group users.
|
||||||
|
self.fields['users'].initial = self.instance.user_set.all()
|
||||||
|
|
||||||
|
# Add the users field.
|
||||||
|
users = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=User.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=FilteredSelectMultiple('users', False),
|
||||||
|
label=_('Users'),
|
||||||
|
help_text=_('Select which users are assigned to this group')
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_m2m(self):
|
||||||
|
# Add the users to the Group.
|
||||||
|
|
||||||
|
self.instance.user_set.set(self.cleaned_data['users'])
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Default save
|
||||||
|
instance = super().save()
|
||||||
|
# Save many-to-many data
|
||||||
|
self.save_m2m()
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class RoleGroupAdmin(admin.ModelAdmin):
|
||||||
|
"""
|
||||||
|
Custom admin interface for the Group model
|
||||||
|
"""
|
||||||
|
|
||||||
|
form = InvenTreeGroupAdminForm
|
||||||
|
|
||||||
|
inlines = [
|
||||||
|
RuleSetInline,
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_formsets_with_inlines(self, request, obj=None):
|
||||||
|
for inline in self.get_inline_instances(request, obj):
|
||||||
|
# Hide RuleSetInline in the 'Add role' view
|
||||||
|
if not isinstance(inline, RuleSetInline) or obj is not None:
|
||||||
|
yield inline.get_formset(request, obj), inline
|
||||||
|
|
||||||
|
filter_horizontal = ['permissions']
|
||||||
|
|
||||||
|
# Save inlines before model
|
||||||
|
# https://stackoverflow.com/a/14860703/12794913
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
if obj is not None:
|
||||||
|
# Save model immediately only if in 'Add role' view
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
else:
|
||||||
|
pass # don't actually save the parent instance
|
||||||
|
|
||||||
|
def save_formset(self, request, form, formset, change):
|
||||||
|
formset.save() # this will save the children
|
||||||
|
form.instance.save() # form.instance is the parent
|
||||||
|
|
||||||
|
|
||||||
|
class InvenTreeUserAdmin(UserAdmin):
|
||||||
|
"""
|
||||||
|
Custom admin page for the User model.
|
||||||
|
|
||||||
|
Hides the "permissions" view as this is now handled
|
||||||
|
entirely by groups and RuleSets.
|
||||||
|
|
||||||
|
(And it's confusing!)
|
||||||
|
"""
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(None, {'fields': ('username', 'password')}),
|
||||||
|
(_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
|
||||||
|
(_('Permissions'), {
|
||||||
|
'fields': ('is_active', 'is_staff', 'is_superuser', 'groups'),
|
||||||
|
}),
|
||||||
|
(_('Important dates'), {'fields': ('last_login', 'date_joined')}),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.unregister(Group)
|
||||||
|
admin.site.register(Group, RoleGroupAdmin)
|
||||||
|
|
||||||
|
admin.site.unregister(User)
|
||||||
|
admin.site.register(User, InvenTreeUserAdmin)
|
||||||
|
@ -1,8 +1,33 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class UsersConfig(AppConfig):
|
class UsersConfig(AppConfig):
|
||||||
name = 'users'
|
name = 'users'
|
||||||
|
|
||||||
|
def ready(self):
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.assign_permissions()
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def assign_permissions(self):
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
from users.models import RuleSet, update_group_roles
|
||||||
|
|
||||||
|
# First, delete any rule_set objects which have become outdated!
|
||||||
|
for rule in RuleSet.objects.all():
|
||||||
|
if rule.name not in RuleSet.RULESET_NAMES:
|
||||||
|
print("need to delete:", rule.name)
|
||||||
|
rule.delete()
|
||||||
|
|
||||||
|
# Update group permission assignments for all groups
|
||||||
|
for group in Group.objects.all():
|
||||||
|
|
||||||
|
update_group_roles(group)
|
||||||
|
31
InvenTree/users/migrations/0001_initial.py
Normal file
31
InvenTree/users/migrations/0001_initial.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2020-10-03 13:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0011_update_proxy_permissions'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='RuleSet',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(choices=[('general', 'General'), ('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('supplier', 'Suppliers'), ('purchase_order', 'Purchase Orders'), ('customer', 'Customers'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50)),
|
||||||
|
('can_view', models.BooleanField(default=True, help_text='Permission to view items', verbose_name='View')),
|
||||||
|
('can_add', models.BooleanField(default=False, help_text='Permission to add items', verbose_name='Create')),
|
||||||
|
('can_change', models.BooleanField(default=False, help_text='Permissions to edit items', verbose_name='Update')),
|
||||||
|
('can_delete', models.BooleanField(default=False, help_text='Permission to delete items', verbose_name='Delete')),
|
||||||
|
('group', models.ForeignKey(help_text='Group', on_delete=django.db.models.deletion.CASCADE, related_name='rule_sets', to='auth.Group')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'unique_together': {('name', 'group')},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
18
InvenTree/users/migrations/0002_auto_20201004_0158.py
Normal file
18
InvenTree/users/migrations/0002_auto_20201004_0158.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.0.7 on 2020-10-04 01:58
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('users', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ruleset',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(choices=[('admin', 'Admin'), ('part', 'Parts'), ('stock', 'Stock'), ('build', 'Build Orders'), ('purchase_order', 'Purchase Orders'), ('sales_order', 'Sales Orders')], help_text='Permission set', max_length=50),
|
||||||
|
),
|
||||||
|
]
|
0
InvenTree/users/migrations/__init__.py
Normal file
0
InvenTree/users/migrations/__init__.py
Normal file
@ -1 +1,317 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.contrib.auth.models import Group, Permission
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.db.models.signals import post_save
|
||||||
|
|
||||||
|
|
||||||
|
class RuleSet(models.Model):
|
||||||
|
"""
|
||||||
|
A RuleSet is somewhat like a superset of the django permission class,
|
||||||
|
in that in encapsulates a bunch of permissions.
|
||||||
|
|
||||||
|
There are *many* apps models used within InvenTree,
|
||||||
|
so it makes sense to group them into "roles".
|
||||||
|
|
||||||
|
These roles translate (roughly) to the menu options available.
|
||||||
|
|
||||||
|
Each role controls permissions for a number of database tables,
|
||||||
|
which are then handled using the normal django permissions approach.
|
||||||
|
"""
|
||||||
|
|
||||||
|
RULESET_CHOICES = [
|
||||||
|
('admin', _('Admin')),
|
||||||
|
('part', _('Parts')),
|
||||||
|
('stock', _('Stock')),
|
||||||
|
('build', _('Build Orders')),
|
||||||
|
('purchase_order', _('Purchase Orders')),
|
||||||
|
('sales_order', _('Sales Orders')),
|
||||||
|
]
|
||||||
|
|
||||||
|
RULESET_NAMES = [
|
||||||
|
choice[0] for choice in RULESET_CHOICES
|
||||||
|
]
|
||||||
|
|
||||||
|
RULESET_MODELS = {
|
||||||
|
'admin': [
|
||||||
|
'auth_group',
|
||||||
|
'auth_user',
|
||||||
|
'auth_permission',
|
||||||
|
'authtoken_token',
|
||||||
|
'users_ruleset',
|
||||||
|
],
|
||||||
|
'part': [
|
||||||
|
'part_part',
|
||||||
|
'part_bomitem',
|
||||||
|
'part_partcategory',
|
||||||
|
'part_partattachment',
|
||||||
|
'part_partsellpricebreak',
|
||||||
|
'part_parttesttemplate',
|
||||||
|
'part_partparametertemplate',
|
||||||
|
'part_partparameter',
|
||||||
|
],
|
||||||
|
'stock': [
|
||||||
|
'stock_stockitem',
|
||||||
|
'stock_stocklocation',
|
||||||
|
'stock_stockitemattachment',
|
||||||
|
'stock_stockitemtracking',
|
||||||
|
'stock_stockitemtestresult',
|
||||||
|
],
|
||||||
|
'build': [
|
||||||
|
'part_part',
|
||||||
|
'part_partcategory',
|
||||||
|
'part_bomitem',
|
||||||
|
'build_build',
|
||||||
|
'build_builditem',
|
||||||
|
'stock_stockitem',
|
||||||
|
'stock_stocklocation',
|
||||||
|
],
|
||||||
|
'purchase_order': [
|
||||||
|
'company_company',
|
||||||
|
'company_supplierpart',
|
||||||
|
'company_supplierpricebreak',
|
||||||
|
'order_purchaseorder',
|
||||||
|
'order_purchaseorderattachment',
|
||||||
|
'order_purchaseorderlineitem',
|
||||||
|
],
|
||||||
|
'sales_order': [
|
||||||
|
'company_company',
|
||||||
|
'order_salesorder',
|
||||||
|
'order_salesorderattachment',
|
||||||
|
'order_salesorderlineitem',
|
||||||
|
'order_salesorderallocation',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Database models we ignore permission sets for
|
||||||
|
RULESET_IGNORE = [
|
||||||
|
# Core django models (not user configurable)
|
||||||
|
'admin_logentry',
|
||||||
|
'contenttypes_contenttype',
|
||||||
|
'sessions_session',
|
||||||
|
|
||||||
|
# Models which currently do not require permissions
|
||||||
|
'common_colortheme',
|
||||||
|
'common_currency',
|
||||||
|
'common_inventreesetting',
|
||||||
|
'company_contact',
|
||||||
|
'label_stockitemlabel',
|
||||||
|
'report_reportasset',
|
||||||
|
'report_testreport',
|
||||||
|
'part_partstar',
|
||||||
|
]
|
||||||
|
|
||||||
|
RULE_OPTIONS = [
|
||||||
|
'can_view',
|
||||||
|
'can_add',
|
||||||
|
'can_change',
|
||||||
|
'can_delete',
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = (
|
||||||
|
('name', 'group'),
|
||||||
|
)
|
||||||
|
|
||||||
|
name = models.CharField(
|
||||||
|
max_length=50,
|
||||||
|
choices=RULESET_CHOICES,
|
||||||
|
blank=False,
|
||||||
|
help_text=_('Permission set')
|
||||||
|
)
|
||||||
|
|
||||||
|
group = models.ForeignKey(
|
||||||
|
Group,
|
||||||
|
related_name='rule_sets',
|
||||||
|
blank=False, null=False,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
help_text=_('Group'),
|
||||||
|
)
|
||||||
|
|
||||||
|
can_view = models.BooleanField(verbose_name=_('View'), default=True, help_text=_('Permission to view items'))
|
||||||
|
|
||||||
|
can_add = models.BooleanField(verbose_name=_('Create'), default=False, help_text=_('Permission to add items'))
|
||||||
|
|
||||||
|
can_change = models.BooleanField(verbose_name=_('Update'), default=False, help_text=_('Permissions to edit items'))
|
||||||
|
|
||||||
|
can_delete = models.BooleanField(verbose_name=_('Delete'), default=False, help_text=_('Permission to delete items'))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_model_permission_string(model, permission):
|
||||||
|
"""
|
||||||
|
Construct the correctly formatted permission string,
|
||||||
|
given the app_model name, and the permission type.
|
||||||
|
"""
|
||||||
|
|
||||||
|
app, model = model.split('_')
|
||||||
|
|
||||||
|
return "{app}.{perm}_{model}".format(
|
||||||
|
app=app,
|
||||||
|
perm=permission,
|
||||||
|
model=model
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_models(self):
|
||||||
|
"""
|
||||||
|
Return the database tables / models that this ruleset covers.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.RULESET_MODELS.get(self.name, [])
|
||||||
|
|
||||||
|
|
||||||
|
def update_group_roles(group, debug=False):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Iterates through all of the RuleSets associated with the group,
|
||||||
|
and ensures that the correct permissions are either applied or removed from the group.
|
||||||
|
|
||||||
|
This function is called under the following conditions:
|
||||||
|
|
||||||
|
a) Whenever the InvenTree database is launched
|
||||||
|
b) Whenver the group object is updated
|
||||||
|
|
||||||
|
The RuleSet model has complete control over the permissions applied to any group.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# List of permissions already associated with this group
|
||||||
|
group_permissions = set()
|
||||||
|
|
||||||
|
# Iterate through each permission already assigned to this group,
|
||||||
|
# and create a simplified permission key string
|
||||||
|
for p in group.permissions.all():
|
||||||
|
(permission, app, model) = p.natural_key()
|
||||||
|
|
||||||
|
permission_string = '{app}.{perm}'.format(
|
||||||
|
app=app,
|
||||||
|
perm=permission
|
||||||
|
)
|
||||||
|
|
||||||
|
group_permissions.add(permission_string)
|
||||||
|
|
||||||
|
# List of permissions which must be added to the group
|
||||||
|
permissions_to_add = set()
|
||||||
|
|
||||||
|
# List of permissions which must be removed from the group
|
||||||
|
permissions_to_delete = set()
|
||||||
|
|
||||||
|
def add_model(name, action, allowed):
|
||||||
|
"""
|
||||||
|
Add a new model to the pile:
|
||||||
|
|
||||||
|
args:
|
||||||
|
name - The name of the model e.g. part_part
|
||||||
|
action - The permission action e.g. view
|
||||||
|
allowed - Whether or not the action is allowed
|
||||||
|
"""
|
||||||
|
|
||||||
|
if action not in ['view', 'add', 'change', 'delete']:
|
||||||
|
raise ValueError("Action {a} is invalid".format(a=action))
|
||||||
|
|
||||||
|
permission_string = RuleSet.get_model_permission_string(model, action)
|
||||||
|
|
||||||
|
if allowed:
|
||||||
|
|
||||||
|
# An 'allowed' action is always preferenced over a 'forbidden' action
|
||||||
|
if permission_string in permissions_to_delete:
|
||||||
|
permissions_to_delete.remove(permission_string)
|
||||||
|
|
||||||
|
if permission_string not in group_permissions:
|
||||||
|
permissions_to_add.add(permission_string)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
# A forbidden action will be ignored if we have already allowed it
|
||||||
|
if permission_string not in permissions_to_add:
|
||||||
|
|
||||||
|
if permission_string in group_permissions:
|
||||||
|
permissions_to_delete.add(permission_string)
|
||||||
|
|
||||||
|
# Get all the rulesets associated with this group
|
||||||
|
for r in RuleSet.RULESET_CHOICES:
|
||||||
|
|
||||||
|
rulename = r[0]
|
||||||
|
|
||||||
|
try:
|
||||||
|
ruleset = RuleSet.objects.get(group=group, name=rulename)
|
||||||
|
except RuleSet.DoesNotExist:
|
||||||
|
# Create the ruleset with default values (if it does not exist)
|
||||||
|
ruleset = RuleSet.objects.create(group=group, name=rulename)
|
||||||
|
|
||||||
|
# Which database tables does this RuleSet touch?
|
||||||
|
models = ruleset.get_models()
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
# Keep track of the available permissions for each model
|
||||||
|
|
||||||
|
add_model(model, 'view', ruleset.can_view)
|
||||||
|
add_model(model, 'add', ruleset.can_add)
|
||||||
|
add_model(model, 'change', ruleset.can_change)
|
||||||
|
add_model(model, 'delete', ruleset.can_delete)
|
||||||
|
|
||||||
|
def get_permission_object(permission_string):
|
||||||
|
"""
|
||||||
|
Find the permission object in the database,
|
||||||
|
from the simplified permission string
|
||||||
|
|
||||||
|
Args:
|
||||||
|
permission_string - a simplified permission_string e.g. 'part.view_partcategory'
|
||||||
|
|
||||||
|
Returns the permission object in the database associated with the permission string
|
||||||
|
"""
|
||||||
|
|
||||||
|
(app, perm) = permission_string.split('.')
|
||||||
|
|
||||||
|
(permission_name, model) = perm.split('_')
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_type = ContentType.objects.get(app_label=app, model=model)
|
||||||
|
permission = Permission.objects.get(content_type=content_type, codename=perm)
|
||||||
|
except ContentType.DoesNotExist:
|
||||||
|
print(f"Error: Could not find permission matching '{permission_string}'")
|
||||||
|
permission = None
|
||||||
|
|
||||||
|
return permission
|
||||||
|
|
||||||
|
# Add any required permissions to the group
|
||||||
|
for perm in permissions_to_add:
|
||||||
|
|
||||||
|
permission = get_permission_object(perm)
|
||||||
|
|
||||||
|
group.permissions.add(permission)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f"Adding permission {perm} to group {group.name}")
|
||||||
|
|
||||||
|
# Remove any extra permissions from the group
|
||||||
|
for perm in permissions_to_delete:
|
||||||
|
|
||||||
|
permission = get_permission_object(perm)
|
||||||
|
|
||||||
|
group.permissions.remove(permission)
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
print(f"Removing permission {perm} from group {group.name}")
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Group)
|
||||||
|
def create_missing_rule_sets(sender, instance, **kwargs):
|
||||||
|
"""
|
||||||
|
Called *after* a Group object is saved.
|
||||||
|
As the linked RuleSet instances are saved *before* the Group,
|
||||||
|
then we can now use these RuleSet values to update the
|
||||||
|
group permissions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
update_group_roles(instance)
|
||||||
|
@ -1,4 +1,157 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
# from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.apps import apps
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
|
|
||||||
|
from users.models import RuleSet
|
||||||
|
|
||||||
|
|
||||||
|
class RuleSetModelTest(TestCase):
|
||||||
|
"""
|
||||||
|
Some simplistic tests to ensure the RuleSet model is setup correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_ruleset_models(self):
|
||||||
|
|
||||||
|
keys = RuleSet.RULESET_MODELS.keys()
|
||||||
|
|
||||||
|
# Check if there are any rulesets which do not have models defined
|
||||||
|
|
||||||
|
missing = [name for name in RuleSet.RULESET_NAMES if name not in keys]
|
||||||
|
|
||||||
|
if len(missing) > 0:
|
||||||
|
print("The following rulesets do not have models assigned:")
|
||||||
|
for m in missing:
|
||||||
|
print("-", m)
|
||||||
|
|
||||||
|
# Check if models have been defined for a ruleset which is incorrect
|
||||||
|
extra = [name for name in keys if name not in RuleSet.RULESET_NAMES]
|
||||||
|
|
||||||
|
if len(extra) > 0:
|
||||||
|
print("The following rulesets have been improperly added to RULESET_MODELS:")
|
||||||
|
for e in extra:
|
||||||
|
print("-", e)
|
||||||
|
|
||||||
|
# Check that each ruleset has models assigned
|
||||||
|
empty = [key for key in keys if len(RuleSet.RULESET_MODELS[key]) == 0]
|
||||||
|
|
||||||
|
if len(empty) > 0:
|
||||||
|
print("The following rulesets have empty entries in RULESET_MODELS:")
|
||||||
|
for e in empty:
|
||||||
|
print("-", e)
|
||||||
|
|
||||||
|
self.assertEqual(len(missing), 0)
|
||||||
|
self.assertEqual(len(extra), 0)
|
||||||
|
self.assertEqual(len(empty), 0)
|
||||||
|
|
||||||
|
def test_model_names(self):
|
||||||
|
"""
|
||||||
|
Test that each model defined in the rulesets is valid,
|
||||||
|
based on the database schema!
|
||||||
|
"""
|
||||||
|
|
||||||
|
available_models = apps.get_models()
|
||||||
|
|
||||||
|
available_tables = set()
|
||||||
|
|
||||||
|
# Extract each available database model and construct a formatted string
|
||||||
|
for model in available_models:
|
||||||
|
label = model.objects.model._meta.label
|
||||||
|
label = label.replace('.', '_').lower()
|
||||||
|
available_tables.add(label)
|
||||||
|
|
||||||
|
assigned_models = set()
|
||||||
|
|
||||||
|
# Now check that each defined model is a valid table name
|
||||||
|
for key in RuleSet.RULESET_MODELS.keys():
|
||||||
|
|
||||||
|
models = RuleSet.RULESET_MODELS[key]
|
||||||
|
|
||||||
|
for m in models:
|
||||||
|
|
||||||
|
assigned_models.add(m)
|
||||||
|
|
||||||
|
missing_models = set()
|
||||||
|
|
||||||
|
for model in available_tables:
|
||||||
|
if model not in assigned_models and model not in RuleSet.RULESET_IGNORE:
|
||||||
|
missing_models.add(model)
|
||||||
|
|
||||||
|
if len(missing_models) > 0:
|
||||||
|
print("The following database models are not covered by the defined RuleSet permissions:")
|
||||||
|
for m in missing_models:
|
||||||
|
print("-", m)
|
||||||
|
|
||||||
|
extra_models = set()
|
||||||
|
|
||||||
|
defined_models = set()
|
||||||
|
|
||||||
|
for model in assigned_models:
|
||||||
|
defined_models.add(model)
|
||||||
|
|
||||||
|
for model in RuleSet.RULESET_IGNORE:
|
||||||
|
defined_models.add(model)
|
||||||
|
|
||||||
|
for model in defined_models:
|
||||||
|
if model not in available_tables:
|
||||||
|
extra_models.add(model)
|
||||||
|
|
||||||
|
if len(extra_models) > 0:
|
||||||
|
print("The following RuleSet permissions do not match a database model:")
|
||||||
|
for m in extra_models:
|
||||||
|
print("-", m)
|
||||||
|
|
||||||
|
self.assertEqual(len(missing_models), 0)
|
||||||
|
self.assertEqual(len(extra_models), 0)
|
||||||
|
|
||||||
|
def test_permission_assign(self):
|
||||||
|
"""
|
||||||
|
Test that the permission assigning works!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Create a new group
|
||||||
|
group = Group.objects.create(name="Test group")
|
||||||
|
|
||||||
|
rulesets = group.rule_sets.all()
|
||||||
|
|
||||||
|
# Rulesets should have been created automatically for this group
|
||||||
|
self.assertEqual(rulesets.count(), len(RuleSet.RULESET_CHOICES))
|
||||||
|
|
||||||
|
# Check that all permissions have been assigned permissions?
|
||||||
|
permission_set = set()
|
||||||
|
|
||||||
|
for models in RuleSet.RULESET_MODELS.values():
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
permission_set.add(model)
|
||||||
|
|
||||||
|
# Every ruleset by default sets one permission, the "view" permission set
|
||||||
|
self.assertEqual(group.permissions.count(), len(permission_set))
|
||||||
|
|
||||||
|
# Add some more rules
|
||||||
|
for rule in rulesets:
|
||||||
|
rule.can_add = True
|
||||||
|
rule.can_change = True
|
||||||
|
|
||||||
|
rule.save()
|
||||||
|
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
# There should now be three permissions for each rule set
|
||||||
|
self.assertEqual(group.permissions.count(), 3 * len(permission_set))
|
||||||
|
|
||||||
|
# Now remove *all* permissions
|
||||||
|
for rule in rulesets:
|
||||||
|
rule.can_view = False
|
||||||
|
rule.can_add = False
|
||||||
|
rule.can_change = False
|
||||||
|
rule.can_delete = False
|
||||||
|
|
||||||
|
rule.save()
|
||||||
|
|
||||||
|
group.save()
|
||||||
|
|
||||||
|
# There should now not be any permissions assigned to this group
|
||||||
|
self.assertEqual(group.permissions.count(), 0)
|
||||||
|
Loading…
Reference in New Issue
Block a user