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.6.tar.gz (14.1 kB view hashes)

Uploaded Source

Built Distribution

fractal_roles-1.0.6-py3-none-any.whl (7.2 kB view hashes)

Uploaded Python 3

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