Skip to main content

Django permissions delegation utils.

Project description

Django Legate

Simple to django guardian, but defines user permissions for models based on some general relation.

For example: Company -> Blog post. Add a blog.change_post permission to User+Company to add an ability for user to edit all companies block posts.

Installation

pip install px-django-legate

Depends on: px-django-combinable-groups.

In settings.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,
}

Usage

Assign permissions

from pxd_legate.services import assigner
from pxd_combinable_groups.services import permissions_collector


# To add additional access capabilities to user for some "root" objects:
accesses = assigner.add_access(
  # User identifier
  user.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], # Optional
  group_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! Method set_access optional parameters are optional only if they're None. Empty list [] is also a value. Check comments higher.

Check permissions

Checker service provides Checker class. You will need to create separate checker class for every model your'e going to check access for.

from pxd_legate.services import checker
from pxd_combinable_groups.services import permissions_collector


posts_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 the for_queryset method checker uses with_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 registry

To make your life easier there is a checkers Registry class since [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_annotation as any checker has. The difference is that registry will combine all checkers to check user's access.

from pxd_legate.services import checker, registry

all_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 check
posts_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 permission

Each checker and registry object has a get_permissions methods that returns a set of user available permission identifiers.

from pxd_legate.services import checker, registry

all_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.

from pxd_legate.services import gatherer

# 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 change

If your going to change ObjectAccess manually 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:

from pxd_legate.services import gatherer

any_object = ObjectAccess.objects.first()

# Some changes happening...

gatherer.gather_object_accesses(
  # You should gather accesses in bulk. It's faster.
  [any_object]
)

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

[0.1.4]

Fixed

  • Global permissions check in Checker now uses default auth backends.

[0.1.3]

Added

  • Checker permission gatherers.

[0.1.2]

Added

  • Permissions gatherers.

[0.1.1]

Added

  • Checkers Registry mechanics.

[0.1.0]

Initial version.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

px-django-legate-0.1.4.tar.gz (14.9 kB view details)

Uploaded Source

File details

Details for the file px-django-legate-0.1.4.tar.gz.

File metadata

  • Download URL: px-django-legate-0.1.4.tar.gz
  • Upload date:
  • Size: 14.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/4.0.1 CPython/3.9.12

File hashes

Hashes for px-django-legate-0.1.4.tar.gz
Algorithm Hash digest
SHA256 e5f6759e0e5d9dbd7693c6244ee2a3d2dcf2c86eb470906c423cac69ce379fed
MD5 14d29bc02ca910dd7935e69b11aabfbf
BLAKE2b-256 f02d0270a262528544778d0d95e1d36d28af36a929b07d803620ae55ba56fe77

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page