Relevant concepts

  • ACL

An ACL, short for An Access Control List, contains the definition of permissions that can be performed on an object or a record.

For example, a file object whose ACL is {“Alice”: {“read”: true, “write”: true}, “Bob”: {“read”: true}},

This means that Alice can both read and write to the file, while Bob can only read.

  • RBAC

Role-based Access Control (RBAC) is different from granting permissions to users. Instead, it assigns permissions to roles.

In the RBAC model, “permission” only corresponds to “role”, while users also correspond to “role”. Roles are assigned to users, and then the permissions of roles are managed to complete the decoupling of permissions and users.

  • RBAC0/RBAC1/RBAC2/RBAC3

The main features of RBAC0 are:

The relationship between users and roles is many-to-one or many-to-many.

RBAC1 is characterized by:

Roles can be inherited, forming a tree.

RBAC2 is mainly specific to:

Roles can be mutually exclusive. (cashier and accounting) base constraints. (CEO) prerequisites. (upgrade by layer) run time mutually exclusive. (Only one role is allowed at runtime, three states of water)

RBAC3 is used to unify RABAC1 and RBAC2.

  • Data access
object (row) level permissions 
model (table) level permissions
Copy the code
  • Intuitive representation of permissions

Operation permission: Menu and button on the web system page Data permission: Operations on data records in the Web system

Default django permissions

Django comes with a simple permissions system. It provides a way to assign permissions to specified users and groups of users.

The group and role here are really the same concept

The User object has two many-to-many fields :groups and user_permissions. User objects can access their related objects just like any other Django Model:.

myuser.groups.set([group_list])
myuser.groups.add(group, group, ...)
myuser.groups.remove(group, group, ...)
myuser.groups.clear()
myuser.user_permissions.set([permission_list])
myuser.user_permissions.add(permission, permission, ...)
myuser.user_permissions.remove(permission, permission, ...)
myuser.user_permissions.clear()
Copy the code

Note here: Users can be authorized directly.

Suppose you have an application called foo and a model called Bar. To test the base permissions, you should use:

Add: user. Has_perm ('foo.add_bar') modified: user.has_perm('foo.change_bar') delete: user.has_perm('foo.delete_bar') view: user.has_perm('foo.view_bar')
Copy the code

Default permissions can be extended/masked:

class Person(models.Model):
    class Meta:
        default_permissions = ()
        permissions = [('can_eat_pizzas'.'Can eat pizzas')]
Copy the code

Default_permissions = () blocks the Person defaults of add_person, Change_person, delete_person, and view_person. Permissions = [(‘can_eat_pizzas’, ‘Can eat pizzas’)] adds the can_eat_pizzas permission for Person.

System requirement analysis

  1. Roles have code that can be easily used during programming, such as:
if user.role.has("manager") :
    dosomething()
Copy the code
  1. Permissions can be organized into secondary directories using menus, such as:
* Rights Management - User Management * Add * Edit * Delete * Search - Role Management - Rights Management * Forum Management - Page Management * Add Page * Modify page * View page * Close - Article Management *...Copy the code
  1. Permissions can be used with RESTFul specification for remote interception (without model)
GET /articles/
DELETE /articles/1/
Copy the code

Model implementation

Operation permission section

  1. The Model section is as follows:
class Permission(models.Model):
    """Convention level 1 represents the directory, level 2 represents the page, level 3 represents the button."""
    name = models.CharField(verbose_name='name', max_length=32, blank=True, null=True)
    code = models.CharField(verbose_name='coding', max_length=32, blank=True, null=True)
    higher = models.ForeignKey('self', verbose_name='superior', on_delete=models.CASCADE)
    url = models.CharField(verbose_name='path', max_length=32, blank=True, null=True)
    action = models.CharField(verbose_name='methods', max_length=32, blank=True, null=True)
    ...


class Role(models.Model):
    name = models.CharField(verbose_name='name', max_length=32, blank=True, null=True)
    code = models.CharField(verbose_name='coding', max_length=32, blank=True, null=True)
    permissions = models.ManyToManyField(
        Permission,
        verbose_name='permissions',
        blank=True,
    )
    ...


class User(AbstractUser):
    roles = models.ManyToManyField(
        Role,
        verbose_name='roles',
        blank=True,
    )
    ...
Copy the code
  1. Operation permission interception is as follows:
class RBACMiddleware:

    def __call__(self, request):
        request_url = request.path_info
        request_user = request.user
        for url in settings.SAFE_URL:
            if re.match(url, request_url):
                pass
        # Read database/cache
        if has_permission_url(request_user, request_url):
            pass
        else:
            return render(request, 'page403.html')
Copy the code

Data Permissions section

Data rights are closely combined with services. Generally, unified data rights interception is not required and each service can be used freely. However, data permissions can be abstracted into the following types, standardized use, to achieve configurable.

* Row limit (controls the number of rows that can be affected based on the condition of a column) - owner is_OWner_REQUIRED can only delete his own rows - collaborator is_Teamworker_Required can edit the rows to which team(department) belongs - restricted Is_manager_required Can approve leave of absence for 3 days * Column limits (controls which columns can be affected) - Phone number confidential FILter_phone - salary confidential filter_SALARYCopy the code
  1. The model part
Class Checker(models.model): CHECKER_CLAZZ = ((1, function), (2, expression),) clazz = models.charfield (verbose_name='categories', choices=CHECKER_TYPE, max_length=15, blank=True, null=True)
    name = models.CharField(verbose_name='name', max_length=32, blank=True, null=True)
    code = models.CharField(verbose_name='coding', max_length=32, blank=True, null=True)
    value = models.CharField(verbose_name='numerical value', max_length=32, blank=True, null=True)
    ...
Copy the code
  1. implementation

Simple interception:

def is_owner_required(model, pk_name='pk'):
    def decorator(view_func):
        def wrap(request, *args, **kwargs):
            pk = kwargs.get(pk_name, None)
            o=model.objects.get(pk=pk) #raises ObjectDoesNotExist
            if o.is_owner(request.user):
                return view_func(request, *args, **kwargs)
            else:
                raise PermissionDenied
        return wraps(view_func)(wrap)
    return decorator

def is_teamworker_required(model, pk_name='pk'):
    def decorator(view_func):
        def wrap(request, *args, **kwargs):
            pk = kwargs.get(pk_name, None)
            o=model.objects.get(pk=pk) #raises ObjectDoesNotExist
            if o.is_teamworker(request.user):
                return view_func(request, *args, **kwargs)
            else:
                raise PermissionDenied
        return wraps(view_func)(wrap)
    return decorator

@is_owner_required(Comment)
def delete_comment(request, pk):
    pass
    
@is_teamworker_required(Comment)
def edit_comment(request, pk):
    pass
Copy the code

Complex interception:

def is_manager_required(code, pk_name='pk'):
    def decorator(view_func):
        def wrap(request, *args, **kwargs):
            pk = kwargs.get(pk_name, None)
            o=model.objects.get(pk=pk) #raises ObjectDoesNotExist
            c=checkModel.objects.get(code=code)
            # check request user role limit value
            if c.check(request.user):
                return view_func(request, *args, **kwargs)
            else:
                raise PermissionDenied
        return wraps(view_func)(wrap)
    return decorator
    
@is_manager_required(code="manager_limit_3")
def audit_holiday(request, pk):
    pass
Copy the code

Fully dynamic interception:

def common_required(code, pk_name='pk'):
    def decorator(view_func):
        def wrap(request, *args, **kwargs):
            pk = kwargs.get(pk_name, None)
            o=model.objects.get(pk=pk) #raises ObjectDoesNotExist
            c=checkModel.objects.get(code=code)
            # Dynamic fetching module
            module = __import__(c.value.module_name, fromlist=[c.value.module_class])
            # Dynamically retrieve validation functions
            checker = getattr(module, c.value.name)
            # Execute validation function
            if checker.check(request.user, c.value.number):
                return view_func(request, *args, **kwargs)
            else:
                raise PermissionDenied
        return wraps(view_func)(wrap)
    return decorator
    
@common_required(code="check_user_level")
def dosomething(request, pk):
    pass
Copy the code