Skip to main content

Fractal Roles provides a flexible way to define fine-grained roles & permissions for users of your Python applications.

Project description

Fractal Roles

Fractal Roles provides a flexible way to define fine-grained roles & permissions for users of your Python applications.

PyPI Version Build Status Code Coverage Code Quality

Installation

pip install fractal-roles

Development

Setup the development environment by running:

make deps
pre-commit install

Happy coding.

Occasionally you can run:

make lint

This is not explicitly necessary because the git hook does the same thing.

Do not disable the git hooks upon commit!

Usage

To be able to use Fractal Roles you first need to define which roles are available in your application.
Let's say you have an Admin user and a regular User. You can then create the following roles in your application:

from fractal_roles.models import Role


class Admin(Role):
    ...


class User(Role):
    ...

For now, we skip permissions, we'll get back to it later.

Next you can create a RolesService to install the roles.

from fractal_roles.services import BaseRolesService


class RolesService(BaseRolesService):
    def __init__(self):
        self.roles = [Admin(), User()]

Last but not least we need to define a dataclass for the user's (authentication token) payload:

from dataclasses import dataclass

from fractal_roles.models import TokenPayloadRolesMixin


@dataclass
class TokenPayloadRoles(TokenPayloadRolesMixin):
    sub: str = ""  # JWT's standard claim for the subject of the token (for example, the user id)
    account: str = ""  # a custom claim, in this case, to point to the account where the user belongs to

The application in which this RolesService will be used, needs to provide the payload everytime a user tries to access a so-called endpoint.
When building an API application, the request should contain a header with the authentication token, which usually is in the form of JWT, and should contain the user's assigned role(s).

Verifying a user's payload

Example payload:

{
  "roles": ["user"],
  "sub": "12345",
  "account": "67890"
}

The json above should be loaded into a TokenPayloadRoles object. From now on, when we refer to payload we mean such an object.

When a user tries to access an endpoint, before it actually executes, the application should verify the payload. Suppose the user tries to get the endpoint get_data, then the verification can be done as follows:

roles_service = RolesService()
payload = roles_service.verify(payload, "get_data", "get")  # Note that it returns a payload as well

If the code didn't raise a NotAllowedException, then the payload is now enriched with a specification. You can use that specification to filter the data that can be accessed by get_data to return back to the user.

For example:

data = [...]
return list(filter(payload.specification.is_satisfied_by, data))

When using a real database and, for example, Django to manage it, you can convert the specification into a Django ORM query easily. To do so please check out the specification documentation.

A quick example:

from fractal_specifications.contrib.django.specifications import DjangoOrmSpecificationBuilder


q = DjangoOrmSpecificationBuilder.build(payload.specification)
return Data.objects.filter(q)

We will now dive deeper into permissions, but the way to verify a user's payload stays the same.

Fractal Roles plays very well together with Fractal Tokens. The TokenService can convert a token into a ready to use payload. For more information on how to use tokens, please check out the Fractal Tokens package.

Fine-grained permissions

In the example above we defined the roles Admin and User and we didn't set any permissions. By default, any method (get, post, put, delete) on any endpoint will get an empty specification which is always evaluates to True so no data will be filtered.

To change this, we need to define more specific permissions. Let's say both Admin and User roles may only get their own data, by account_id, and on top of that the User may only get its own created data by created_by. We will also only limit this to the get_data function, which in our case is the only external available endpoint.

from fractal_roles.models import Method, Methods, Role
from fractal_specifications.generic.operators import EqualsSpecification
from fractal_specifications.generic.specification import Specification


def my_account(payload: TokenPayloadRoles) -> Specification:
    return EqualsSpecification(
        "account_id", payload.account
    )


def my_data(payload: TokenPayloadRoles) -> Specification:
    return my_account(payload) & EqualsSpecification(
        "created_by", payload.sub
    )


class Admin(Role):
    get_data = Methods(get=Method(my_account), post=None, put=None, delete=None)


class User(Role):
    get_data = Methods(get=Method(my_data), post=None, put=None, delete=None)

To see this code in action, please check out the examples directory in this repository.

Multiple roles

A user payload may also include multiple roles, for example:

{
  "roles": ["user", "admin"],
  "sub": "12345",
  "account": "67890"
}

The first matched Role, from the perspective of the RolesService, will be used for verification.

In our case, this will be Admin:

class RolesService(BaseRolesService):
    def __init__(self):
        self.roles = [Admin(), User()]  # Admin Role will first be checked against the payload

Alternative approach

The examples above work with predefined methods such as get, post, put and delete (where only get is allowed and the rest raising exceptions). These methods are very useful when building a REST API, but when you're not building a REST API, the Fractal Roles can still be of help.

When building a regular Python application, you might still want to limit the execution of certain function by some users. These boundaries can be described in a UML Use Case diagram, which can also be of help for building REST APIs.

In a Use Case diagram, an Actor (Role) can perform/execute an Action. Let's say we have a use case where a Student can order a pizza. Later on in the process the Student needs to pay for the pizza and the cost will be deducted from his Wallet.

The Wallet is a passive actor, so doesn't need a role, but the Student can perform two actions:

  • Order a pizza
  • Pay for the pizza

Be aware that the cost needs to be deducted from his wallet, not from someone else's.

We'll define the following Role and RolesService:

from fractal_roles.services import RolesService as BaseRolesService


@dataclass
class Action:
    execute: Optional[Method] = None


class Student(Role):
    def __getattr__(self, item):
        return Action()

    order_pizza = Action(execute=Method(my_data))  # reuse of my_data as shown in above examples
    pay_for_pizza = Action(execute=Method(my_data))  # reuse of my_data


class RolesService(BaseRolesService):
    def __init__(self):
        self.roles = [Student()]

    def verify(
        self, payload: TokenPayloadRolesMixin, endpoint: str, method: str = "execute"
    ) -> TokenPayloadRolesMixin:
        return super().verify(payload, endpoint, method)

Notice we replaced the standard Methods class with Action which only contains one method named execute.

From the application we can now call the RolesService as follows:

roles_service = RolesService()

data = [
    Wallet(1, "67890", "12345", 100),
    Wallet(2, "67890", "11111", 1000),
    Wallet(3, "00000", "12345", 10000),
]

payload = TokenPayloadRoles(roles=["student"], account="67890", sub="12345")

payload = roles_service.verify(payload, "order_pizza")

# order pizza in the application

payload = roles_service.verify(payload, "pay_for_pizza")

# deduct cost from the correct wallet, using the Specification in the payload

By not getting an exception, you know you can make the real calls to the backend application. The RolesService will, just like in the other examples, return a Specification in the payload to be used in further processing. Like using the correct wallet for making a deduction.

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

fractal-roles-1.0.5.tar.gz (14.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fractal_roles-1.0.5-py3-none-any.whl (7.0 kB view details)

Uploaded Python 3

File details

Details for the file fractal-roles-1.0.5.tar.gz.

File metadata

  • Download URL: fractal-roles-1.0.5.tar.gz
  • Upload date:
  • Size: 14.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.28.1

File hashes

Hashes for fractal-roles-1.0.5.tar.gz
Algorithm Hash digest
SHA256 8919a72930c10404cfb42595295dde0763007f5ae53b8f899ac6dae4d45a0eb1
MD5 af8e8b1fd3b1c0517b4f3a3dc4a46161
BLAKE2b-256 1e80f05562abe3f1c05f6495225086786ce17ee5f3aa85fcfb4e9957dd4557f1

See more details on using hashes here.

File details

Details for the file fractal_roles-1.0.5-py3-none-any.whl.

File metadata

  • Download URL: fractal_roles-1.0.5-py3-none-any.whl
  • Upload date:
  • Size: 7.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.28.1

File hashes

Hashes for fractal_roles-1.0.5-py3-none-any.whl
Algorithm Hash digest
SHA256 454b1871d5f5476aac3e6e20bd6753649ddaa1705f63c703e2a6da1ba4664ac0
MD5 762ac8458a5fd8f0f89a8c28247eb9e0
BLAKE2b-256 8073c13cd48f1ef543c04fd35cccd65b290bdc1750684c96470e52e5924c5744

See more details on using hashes here.

Supported by

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