Metadata-Version: 2.1
Name: px-access-scopes
Version: 0.1.6
Summary: Simple access-scopes utility package.
Home-page: UNKNOWN
Author: Alex Tkachenko
Author-email: preusx.dev@gmail.com
License: MIT License
Platform: UNKNOWN
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Python :: 3
Classifier: Intended Audience :: Developers
Classifier: Topic :: Utilities
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.6
Description-Content-Type: text/markdown
Provides-Extra: dev
License-File: LICENSE

# Access scopes

Utility package for access scopes definition and checking.

## Installation

```sh
pip install px-access-scopes
```

## Usage

Defining access scopes:

`access_scopes.py`

```python
from px_access_scopes import ScopeRegistry, ScopeDomain, auto, raw

# Creating root access scopes registry.
root = ScopeRegistry.create_root(ScopeDomain('TOKENS'))

# Tokens can be any Enum or even a simple object.
@root.nest('TOKENS')
class Tokens(Enum):
  AUTO1 = auto
  AUTO2 = auto
  RAW = raw('RAW')
  FIXED = 'SOME'

@Tokens.nest('NESTED')
class Nested:
  AUTO = auto
  SOME = 'OTHER'
```

Defining scopes aggregates(roles):

`access_aggregates.py`

```python
from px_access_scopes import Aggregates

from .access_scopes import Tokens, Nested


class Roles(Aggregates):
  Simple = Aggregate('Simple')
  First = Aggregate('First')

Roles.Simple.add(Tokens.RAW)
Roles.First.add(Tokens.RAW)
Roles.First.add(Nested.SOME)
```

Run checkers whenever you need:

```python
from px_access_scopes import (
  ScopesCheckRunner, scopes_checker, aggregates_checker, HierarchyChecker,
  domain_path_hierarchy_lookup,
)
from .access_scopes import Tokens, Nested
from .access_aggregates import Roles

# Defining a checker. The result is just a simple callable.
# ScopesCheckRunner receives special checker runners list and evaluates them,
# until it founds a match.
checker = ScopesCheckRunner((
  HierarchyChecker((
    scopes_checker,
    aggregates_checker,
  ), hierarchy_lookup=domain_path_hierarchy_lookup),
))

USER1 = {'scopes': [Nested.AUTO]}
USER2 = {'scopes': [Nested.AUTO], 'aggregates': [Roles.Simple]}
USER3 = {'scopes': [Nested.AUTO], 'aggregates': [Roles.First]}

# It receives scopes list and kwargs, that internal checkers need to make decision.
checker((Nested.AUTO,), **USER1) # > True
checker((Nested.SOME,), **USER1) # > False

checker((Tokens.RAW,), **USER2) # > True
checker((Tokens.RAW,), **USER3) # > True

checker((Nested.AUTO,), **USER3) # > True
checker((Nested.SOME,), **USER3) # > True
```

### Django

Registering scopes registries and access tokens aggregates.

On every `manage.py migrate` auth Permissions and Groups will be autogenerated based on registered definitions.

`settings.py`

```python
PX_ACCESS_TOKENS_REGISTRIES = [
  'access_scopes.staff_root',
]
PX_ACCESS_TOKENS_AGGREGATES = [
  'access_scopes.Roles',
]
```

`access_scopes.py`

```python
from django.utils.translation import pgettext_lazy
from django.db.models.enums import TextChoices

# Has it's own, a little bit improved for django implementations:
from px_access_scopes.contrib.django import Aggregate, Aggregates, ScopeRegistry


class Roles(Aggregates):
  # First parameter for any django aggregate is group identifier.
  # Second is a key name, and the third one is an aggregate's verbose_name.
  Admin = Aggregate(5000, 'Admin', pgettext_lazy('staff', 'Admin'))
  Owner = Aggregate(4900, 'Owner', pgettext_lazy('staff', 'Owner'))
  Reader = Aggregate(4800, 'Reader', pgettext_lazy('staff', 'Reader'))


staff_root = ScopeRegistry.create_root('STAFF', pgettext_lazy('staff', 'Staff'))


# TextChoices is also a Enum, but with labels so better use a django-specific registry.
@staff_root.nest('USERS', pgettext_lazy('staff', 'Users'))
class Users(TextChoices):
  VIEW = 'VIEW', pgettext_lazy('staff', 'View')
  CHANGE = 'CHANGE', pgettext_lazy('staff', 'Change')
  CHANGE_OWN = 'CHANGE_OWN', pgettext_lazy('staff', 'Change own')
  DISABLE = 'DISABLE', pgettext_lazy('staff', 'Disable')


SHARED = {Users.CHANGE_OWN}


Roles.Admin.update(
  SHARED
  | {Users.CHANGE, Users.VIEW, Users.DISABLE}
)
Roles.Owner.update(
  SHARED
  | {Users.CHANGE_OWN, Users.VIEW}
)
Roles.Reader.update(
  SHARED
)
```

And so now you may run a checker for any user. Internally django checker will use user's `.has_perm`. So this way administrators cay manage access for any user.

```python
from px_access_scopes import (
  ScopesChecker, HierarchyChecker, ScopesCheckRunner,
  MultiregistryHierarchyLookup
)
from px_access_scopes.contrib.django import ScopeDomain, user_checker
# All registries, that you've registered in config
from px_access_scopes.contrib.django.globals import registries

from .access_scopes import Users


# Simple checker here.
checker: ScopesChecker = ScopesCheckRunner((
  HierarchyChecker(
    # Django-specific checker that calls `user.has_perm`.
    (user_checker,),
    hierarchy_lookup=MultiregistryHierarchyLookup(
      # This might be any registries, not default ones.
      registries=registries
    )
  ),
))


def my_view(request):
  can = checker(
    # `.permission` - is the django's permission string.
    (User.CHANGE.permission,),
    # User kwarg is required for `user_checker`.
    user=request.user,
  )

  if not can:
    raise ...
  ...
```

For an easier usage scope on frontend there is an export mechanics:

```python
from px_access_scopes.contrib.django.export import export_scopes
```

Function `export_scopes` exports all scopes from all registered registries. It has two modes: shorter one `export_scopes(as_leaves=True)` and more verbose and full `export_scopes(as_leaves=False)`. It's for you to decide which one is preferable.

### DRF

For django rest framework there are ready-to use permission classes.

They use the same checker mechanics as described above in django checking section.

`some_views.py`

```python
from rest_framework.permissions import IsAuthenticated

from .access_scopes import Users


class UserUpdateDestroyAPIView:
  permission_classes = (
    IsAuthenticated
    &
    (
      # You may pass multiple scopes here.
      # Checker will be evaluated only for passed methods.
      # `methods` keyword is optional as by default it will check permission
      # for every possible method.
      ScopePermission.from_scopes(Users.VIEW, methods=('GET',))
      |
      ScopePermission.from_scopes(Users.CHANGE, methods=('PUT', 'PATCH))
      |
      ScopePermission.from_scopes(Users.CHANGE_OWN, methods=('PUT', 'PATCH))
      |
      ScopePermission.from_scopes(Users.DISABLE, methods=('DELETE',))
    ),
  )
  serializer_class = UserSerializer
```


