Package: px-django-legate
Django LegateSimple to django guardian, but defines user permissions for models based on some general relation.For example: Company -> Blog post. Add ablog.change_postpermission to User+Company to add an ability for user to edit all companies block posts.Installationpipinstallpx-django-legateDepends on:px-django-combinable-groups.Insettings.py:INSTALLED_APPS+=[# ...'django.contrib.auth',# ...# Depends on:'pxd_tree.adjacency_list','pxd_combinable_groups','pxd_legate',]PXD_LEGATE={# Will automatically add content types to admin interface.'ADMIN_REGISTER_CONTENT_TYPE':True,# Will automatically add permissions to admin interface.'ADMIN_REGISTER_PERMISSION':True,}UsageAssign permissionsfrompxd_legate.servicesimportassignerfrompxd_combinable_groups.servicesimportpermissions_collector# To add additional access capabilities to user for some "root" objects:accesses=assigner.add_access(# User identifieruser.id,# Any number of objects to add access to[company_1,company_2],# List of additional permissions to add. Optional.permission_ids=permissions_collector.keys_to_ids(('blog.view_post','blog.change_post')),# You may also add groups, not only the permissions. Optional.group_ids=[1,2,3],)# Opposite to adding access, you may also partially withdraw it:accesses=assigner.withdraw_access(user.id,[company_1],permission_ids=[4,5],# Optionalgroup_ids=None,# Optional)# To completely remove user access to some root object there is `remove_access`# method:assigner.remove_access(user.id,[company_2])# This method sets user permissions "strictly" to a provided values:accesses=assigner.set_access(user.id,[company_1],# Whatever the permissions user had before, now he will have only a# 'blog.view_post' permission.# Optional. But be careful! Passing an empty list `[]` will remove# all permissions at all. Only `None` will tell method that there is# no need to do something with `permission_ids`.permission_ids=permissions_collector.keys_to_ids(['blog.view_post']),# Same as for `permission_ids` goes here.# With such a value we removing groups from access object entirely.group_ids=[],# For such value groups wil stay untouched.group_ids=None,)Be careful!Methodset_accessoptional parameters are optional only if they'reNone. Empty list[]is also a value. Check comments higher.Check permissionsChecker service providesCheckerclass. You will need to create separate checker class for every model your'e going to check access for.frompxd_legate.servicesimportcheckerfrompxd_combinable_groups.servicesimportpermissions_collectorposts_checker=checker.Checker(# Model for what your'e going to check user's access.Post,# Full path to a root object to check access against.'department__company',# Root model to check access info. Optional.root_model=Company,# Special comparator object. Instance of `checker.CheckCMP`.# It will be used to compare passed permission_ids with existing ones.# There are 2 built in:# - `checker.ALL_CMP` - User must have all permission_ids.# - `checker.ANY_CMP` - User must have any of provided permission_ids.cmp=checker.ALL_CMP,# Determines whether the user personal permissions should be checked.# So in case of user has it's own permission not to add additional# check mechanics into the query.# It's `True` by default, but there might be cases when there is no such# check required.should_check_global=True,)# To check whether the user can do something with some object:can_edit=posts_checker.for_object(# Object to check access to.some_post,# User.user,# Permission ids to check user's abilities. Optional.permission_ids=permissions_collector.keys_to_ids(['blog.change_post']),# For cases with a different comparison method you may provide a custom# comparator. Event without `permission_ids` at all.cmp=None,)# QuerySet can also be filtered base on the user's ability to do# something with it:only_editable=posts_checker.for_queryset(# QuerySet to filter.Post.objects.all(),# User.user,# Permission ids to check user's abilities. Optional.permission_ids=permissions_collector.keys_to_ids(['blog.change_post']),# For cases with a different comparison method you may provide a custom# comparator. Event without `permission_ids` at all.cmp=None,# It will be passed to a `with_annotation` method(see next).annotation_field_name=None,)Underneath thefor_querysetmethod checker useswith_annotation.
It could be used for more complex situations.
For example some model has two root models, where user access can be defined.posts_department_checker=checker.Checker(Post,'department',root_model=Department,)company_field,query=posts_checker.with_annotation(Post.objects.all(),user,permission_ids=permissions_collector.keys_to_ids(['blog.change_post']),cmp=None,# Name of the field that check annotation will be inserted into.# Could be empty. In that case it will be auto-generated.annotation_field_name=None,)department_field,query=posts_department_checker.with_annotation(# Query from above will again be "checked".query,user,permission_ids=permissions_collector.keys_to_ids(['blog.change_post']),)only_editable=query.filter(# It has access through the company.models.Q(**{company_field:True})|# Or# Through it's department.models.Q(**{department_field:True}))Checkers registryTo make your life easier there is a checkers Registry classsince [0.1.1].It combines separate checker calls into a single object with identical interface.Registry can be used for multiple models and also there could be any number of checkers to run for a particular model.Registry class has the same methods.for_object,.for_queryset,.with_annotationas any checker has. The difference is that registry will combine all checkers to check user's access.frompxd_legate.servicesimportchecker,registryall_registry=registry.Registry(# Comparators for registry has similar interface as checkers CMP objects,# but they are different in terms of passed values, so be careful.# By default any passed checker makes object accessible:cmp=registry.ANY_CMP,# Should registry raise exception if there is no checker available for# currently passed object's model.# By default mechanics ignores that situation and marks all objects as# accessible.raise_noncheckable=False,)# Adding a posts checker to registry.posts_checker=all_registry.add(checker.Checker(Post,'department__company',root_model=Company,))# Another posts checker added. Now they both will checkposts_department_checker=all_registry.add(checker.Checker(Post,'department',root_model=Department,))# Added checker for other model. There will be no conflicts.# Registry will use only the checkers for a currently passed model for any# access check run.department_checker=all_registry.add(checker.Checker(Department,'company',root_model=Company,))# Here both previously registered checkers for posts will run and if any# of it will be successful then that object will be resolved as accessible.posts=all_registry.for_queryset(Post.objects.all(),user,permission_ids=permissions_collector.keys_to_ids(['blog.change_post']),)# `department_checker` will run here.departments=all_registry.for_queryset(Department.objects.all(),user,permission_ids=permissions_collector.keys_to_ids(['organization.change_department']),)# As there is no checkers for the Company model there is nothing will# happen here and the return value will be the same queryset that was# passed down.companies=all_registry.for_queryset(Company.objects.all(),user,permission_ids=permissions_collector.keys_to_ids(['organization.change_company']),# In case when registry was registered with `raise_noncheckable` as True, or# same parameter passed to check method with True value - error will raise.# Here will be used value passed on registry creation.raise_noncheckable=None,)Get user's permissionEach checker and registry object has aget_permissionsmethods that returns a set of user available permission identifiers.frompxd_legate.servicesimportchecker,registryall_registry=registry.Registry()posts_checker=all_registry.add(checker.Checker(Post,'department__company',root_model=Company,))# This will call `get_permissions` method on all checkers that registered# for an object's model.all_registry.get_permissions(post_1,user_object)# > {1,2,3,4}# If there is no checkers available empty set will be returned.all_registry.get_permissions(department_1,user_object)# > set()Also there are a utilities to get user permissions info for a root objects.frompxd_legate.servicesimportgatherer# There are utils to gather user's permissions for root objects.# This will find all ObjectAccess objects:gatherer.objects_for_user(# User object to gather for.user_object,# Root objects list.[any_root_obj_1,any_root_obj_2,...],)# > [ObjectAccess(), ObjectAccess()]# And this one will collect all user's permission ids for any type of objects.gatherer.permissions_for_user(# User object to gather for.user_object,# Root objects list.[any_root_obj_1,any_root_obj_2,...],)# > [1,2,3,4,...]Access manual changeIf your going to changeObjectAccessmanually then after that you must gather actual permissions that it has.In Administration panel it's already done.There is a simple service for that:frompxd_legate.servicesimportgathererany_object=ObjectAccess.objects.first()# Some changes happening...gatherer.gather_object_accesses(# You should gather accesses in bulk. It's faster.[any_object])ChangelogAll notable changes to this project will be documented in this file.The format is based onKeep a Changelog,
and this project adheres toSemantic Versioning.[Unreleased][0.1.4]FixedGlobal permissions check in Checker now uses default auth backends.[0.1.3]AddedChecker permission gatherers.[0.1.2]AddedPermissions gatherers.[0.1.1]AddedCheckers Registry mechanics.[0.1.0]Initial version.