mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-03 22:55:43 +00:00 
			
		
		
		
	Merge pull request #1018 from SchrodingersGat/group-roles
Roles and Permissions
This commit is contained in:
		@@ -138,6 +138,7 @@ INSTALLED_APPS = [
 | 
			
		||||
    'part.apps.PartConfig',
 | 
			
		||||
    'report.apps.ReportConfig',
 | 
			
		||||
    'stock.apps.StockConfig',
 | 
			
		||||
    'users.apps.UsersConfig',
 | 
			
		||||
 | 
			
		||||
    # Third part add-ons
 | 
			
		||||
    'django_filters',               # Extended filter functionality
 | 
			
		||||
 
 | 
			
		||||
@@ -38,4 +38,5 @@ class CompanyConfig(AppConfig):
 | 
			
		||||
                            company.image = None
 | 
			
		||||
                            company.save()
 | 
			
		||||
        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
 | 
			
		||||
    # This crucial otherwise the unit test suite fails!
 | 
			
		||||
    if SupplierPart.objects.count() == 0:
 | 
			
		||||
        print("No SupplierPart objects - skipping")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    print("Reversing migration for manufacturer association")
 | 
			
		||||
@@ -105,7 +104,6 @@ def associate_manufacturers(apps, schema_editor):
 | 
			
		||||
    # Exit if there are no SupplierPart objects
 | 
			
		||||
    # This crucial otherwise the unit test suite fails!
 | 
			
		||||
    if SupplierPart.objects.count() == 0:
 | 
			
		||||
        print("No SupplierPart objects - skipping")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Link a 'manufacturer_name' to a 'Company'
 | 
			
		||||
 
 | 
			
		||||
@@ -37,4 +37,4 @@ class PartConfig(AppConfig):
 | 
			
		||||
                            part.image = None
 | 
			
		||||
                            part.save()
 | 
			
		||||
        except (OperationalError, ProgrammingError):
 | 
			
		||||
            print("Could not generate Part thumbnails")
 | 
			
		||||
            pass
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,135 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
# from __future__ import unicode_literals
 | 
			
		||||
# from django.contrib import admin
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
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 -*-
 | 
			
		||||
from __future__ import unicode_literals
 | 
			
		||||
 | 
			
		||||
from django.db.utils import OperationalError, ProgrammingError
 | 
			
		||||
 | 
			
		||||
from django.apps import AppConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UsersConfig(AppConfig):
 | 
			
		||||
    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 -*-
 | 
			
		||||
 | 
			
		||||
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 -*-
 | 
			
		||||
# 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)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user