# Fractal Roles
> Fractal Roles provides a flexible way to define fine-grained roles & permissions for users of your Python applications.
[![PyPI Version][pypi-image]][pypi-url]
[![Build Status][build-image]][build-url]
[![Code Coverage][coverage-image]][coverage-url]
[![Code Quality][quality-image]][quality-url]
<!-- Badges -->
[pypi-image]: https://img.shields.io/pypi/v/fractal-roles
[pypi-url]: https://pypi.org/project/fractal-roles/
[build-image]: https://github.com/douwevandermeij/fractal-roles/actions/workflows/build.yml/badge.svg
[build-url]: https://github.com/douwevandermeij/fractal-roles/actions/workflows/build.yml
[coverage-image]: https://codecov.io/gh/douwevandermeij/fractal-roles/branch/main/graph/badge.svg?token=Jv2ShlaVf8
[coverage-url]: https://codecov.io/gh/douwevandermeij/fractal-roles
[quality-image]: https://api.codeclimate.com/v1/badges/754713b64573aa47571d/maintainability
[quality-url]: https://codeclimate.com/github/douwevandermeij/fractal-roles
## Installation
```sh
pip install fractal-roles
```
## Development
Setup the development environment by running:
```sh
make deps
pre-commit install
```
Happy coding.
Occasionally you can run:
```sh
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:
```python
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.
```python
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:
```python
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:
```json
{
"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:
```python
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](https://github.com/douwevandermeij/fractal-specifications).
You can use that specification to filter the data that can be accessed by **get_data** to return back to the user.
For example:
```python
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](https://github.com/douwevandermeij/fractal-specifications).
A quick example:
```python
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](https://github.com/douwevandermeij/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.
```python
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:
```json
{
"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**:
```python
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](https://en.wikipedia.org/wiki/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:
```python
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:
```python
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.
Raw data
{
"_id": null,
"home_page": "https://github.com/douwevandermeij/fractal-roles",
"name": "fractal-roles",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.8",
"maintainer_email": null,
"keywords": null,
"author": "Douwe van der Meij",
"author_email": "douwe@karibu-online.nl",
"download_url": "https://files.pythonhosted.org/packages/df/19/79aa3291798d4d9ae01bba9077a525f9c06190d23d7451a717b0ddb01295/fractal_roles-1.0.7.tar.gz",
"platform": null,
"description": "# Fractal Roles\n\n> Fractal Roles provides a flexible way to define fine-grained roles & permissions for users of your Python applications.\n\n[![PyPI Version][pypi-image]][pypi-url]\n[![Build Status][build-image]][build-url]\n[![Code Coverage][coverage-image]][coverage-url]\n[![Code Quality][quality-image]][quality-url]\n\n<!-- Badges -->\n\n[pypi-image]: https://img.shields.io/pypi/v/fractal-roles\n[pypi-url]: https://pypi.org/project/fractal-roles/\n[build-image]: https://github.com/douwevandermeij/fractal-roles/actions/workflows/build.yml/badge.svg\n[build-url]: https://github.com/douwevandermeij/fractal-roles/actions/workflows/build.yml\n[coverage-image]: https://codecov.io/gh/douwevandermeij/fractal-roles/branch/main/graph/badge.svg?token=Jv2ShlaVf8\n[coverage-url]: https://codecov.io/gh/douwevandermeij/fractal-roles\n[quality-image]: https://api.codeclimate.com/v1/badges/754713b64573aa47571d/maintainability\n[quality-url]: https://codeclimate.com/github/douwevandermeij/fractal-roles\n\n## Installation\n\n```sh\npip install fractal-roles\n```\n\n## Development\n\nSetup the development environment by running:\n\n```sh\nmake deps\npre-commit install\n```\n\nHappy coding.\n\nOccasionally you can run:\n\n```sh\nmake lint\n```\n\nThis is not explicitly necessary because the git hook does the same thing.\n\n**Do not disable the git hooks upon commit!**\n\n\n## Usage\n\nTo be able to use Fractal Roles you first need to define which roles are available in your application.\\\nLet's say you have an **Admin** user and a regular **User**. You can then create the following roles in your application:\n\n```python\nfrom fractal_roles.models import Role\n\n\nclass Admin(Role):\n ...\n\n\nclass User(Role):\n ...\n```\n\nFor now, we skip permissions, we'll get back to it later.\n\nNext you can create a RolesService to install the roles.\n\n```python\nfrom fractal_roles.services import BaseRolesService\n\n\nclass RolesService(BaseRolesService):\n def __init__(self):\n self.roles = [Admin(), User()]\n```\n\nLast but not least we need to define a dataclass for the user's (authentication token) payload:\n\n```python\nfrom dataclasses import dataclass\n\nfrom fractal_roles.models import TokenPayloadRolesMixin\n\n\n@dataclass\nclass TokenPayloadRoles(TokenPayloadRolesMixin):\n sub: str = \"\" # JWT's standard claim for the subject of the token (for example, the user id)\n account: str = \"\" # a custom claim, in this case, to point to the account where the user belongs to\n```\n\n**The application in which this RolesService will be used, needs to provide the payload everytime a user tries to access a so-called endpoint.**\\\nWhen building an API application, the request should contain a header with the authentication token, which usually is in the form of JWT,\nand should contain the user's assigned role(s).\n\n### Verifying a user's payload\n\nExample payload:\n\n```json\n{\n \"roles\": [\"user\"],\n \"sub\": \"12345\",\n \"account\": \"67890\"\n}\n```\n\nThe json above should be loaded into a TokenPayloadRoles object. From now on, when we refer to `payload` we mean such an object.\n\nWhen a user tries to access an endpoint, before it actually executes, the application should **verify** the `payload`.\nSuppose the user tries to **get** the endpoint **get_data**, then the verification can be done as follows:\n\n```python\nroles_service = RolesService()\npayload = roles_service.verify(payload, \"get_data\", \"get\") # Note that it returns a payload as well\n```\n\nIf the code didn't raise a `NotAllowedException`, then the `payload` is now enriched with a [specification](https://github.com/douwevandermeij/fractal-specifications).\nYou can use that specification to filter the data that can be accessed by **get_data** to return back to the user.\n\nFor example:\n\n```python\ndata = [...]\nreturn list(filter(payload.specification.is_satisfied_by, data))\n```\n\nWhen using a real database and, for example, Django to manage it, you can convert the specification into a Django ORM query easily.\nTo do so please check out the [specification documentation](https://github.com/douwevandermeij/fractal-specifications).\n\nA quick example:\n```python\nfrom fractal_specifications.contrib.django.specifications import DjangoOrmSpecificationBuilder\n\n\nq = DjangoOrmSpecificationBuilder.build(payload.specification)\nreturn Data.objects.filter(q)\n```\n\nWe will now dive deeper into permissions, but the way to verify a user's payload stays the same.\n\n**Fractal Roles** plays very well together with **Fractal Tokens**. The TokenService can convert a token into a ready to use `payload`.\nFor more information on how to use tokens, please check out the [Fractal Tokens](https://github.com/douwevandermeij/fractal-tokens) package.\n\n### Fine-grained permissions\n\nIn the example above we defined the roles **Admin** and **User** and we didn't set any permissions.\nBy default, any method (get, post, put, delete) on any endpoint will get an empty specification which is always\nevaluates to `True` so no data will be filtered.\n\nTo change this, we need to define more specific permissions. Let's say both **Admin** and **User** roles may only **get**\ntheir own data, by `account_id`, and on top of that the **User** may only **get** its own created data by `created_by`.\nWe will also only limit this to the **get_data** function, which in our case is the only external available endpoint.\n\n```python\nfrom fractal_roles.models import Method, Methods, Role\nfrom fractal_specifications.generic.operators import EqualsSpecification\nfrom fractal_specifications.generic.specification import Specification\n\n\ndef my_account(payload: TokenPayloadRoles) -> Specification:\n return EqualsSpecification(\n \"account_id\", payload.account\n )\n\n\ndef my_data(payload: TokenPayloadRoles) -> Specification:\n return my_account(payload) & EqualsSpecification(\n \"created_by\", payload.sub\n )\n\n\nclass Admin(Role):\n get_data = Methods(get=Method(my_account), post=None, put=None, delete=None)\n\n\nclass User(Role):\n get_data = Methods(get=Method(my_data), post=None, put=None, delete=None)\n```\n\nTo see this code in action, please check out the examples directory in this repository.\n\n### Multiple roles\n\nA user payload may also include multiple roles, for example:\n\n```json\n{\n \"roles\": [\"user\", \"admin\"],\n \"sub\": \"12345\",\n \"account\": \"67890\"\n}\n```\n\nThe first matched Role, from the perspective of the RolesService, will be used for verification.\n\nIn our case, this will be **Admin**:\n\n```python\nclass RolesService(BaseRolesService):\n def __init__(self):\n self.roles = [Admin(), User()] # Admin Role will first be checked against the payload\n```\n\n### Alternative approach\n\nThe examples above work with predefined methods such as **get**, **post**, **put** and **delete** (where only **get** is allowed and the rest raising exceptions).\nThese 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.\n\nWhen building a regular Python application, you might still want to limit the execution of certain function by some users.\nThese boundaries can be described in a [UML Use Case diagram](https://en.wikipedia.org/wiki/Use_case_diagram), which can also be of help for building REST APIs.\n\nIn a Use Case diagram, an **Actor** (Role) can perform/execute an **Action**.\nLet's say we have a use case where a **Student** can **order a pizza**.\nLater on in the process the **Student** needs to **pay for the pizza** and the cost will be deducted from his **Wallet**.\n\nThe **Wallet** is a passive actor, so doesn't need a role, but the **Student** can perform two actions:\n- Order a pizza\n- Pay for the pizza\n\nBe aware that the cost needs to be deducted from **his** wallet, not from someone else's.\n\nWe'll define the following Role and RolesService:\n\n```python\nfrom fractal_roles.services import RolesService as BaseRolesService\n\n\n@dataclass\nclass Action:\n execute: Optional[Method] = None\n\n\nclass Student(Role):\n def __getattr__(self, item):\n return Action()\n\n order_pizza = Action(execute=Method(my_data)) # reuse of my_data as shown in above examples\n pay_for_pizza = Action(execute=Method(my_data)) # reuse of my_data\n\n\nclass RolesService(BaseRolesService):\n def __init__(self):\n self.roles = [Student()]\n\n def verify(\n self, payload: TokenPayloadRolesMixin, endpoint: str, method: str = \"execute\"\n ) -> TokenPayloadRolesMixin:\n return super().verify(payload, endpoint, method)\n```\n\nNotice we replaced the standard `Methods` class with `Action` which only contains one method named `execute`.\n\nFrom the application we can now call the RolesService as follows:\n\n```python\nroles_service = RolesService()\n\ndata = [\n Wallet(1, \"67890\", \"12345\", 100),\n Wallet(2, \"67890\", \"11111\", 1000),\n Wallet(3, \"00000\", \"12345\", 10000),\n]\n\npayload = TokenPayloadRoles(roles=[\"student\"], account=\"67890\", sub=\"12345\")\n\npayload = roles_service.verify(payload, \"order_pizza\")\n\n# order pizza in the application\n\npayload = roles_service.verify(payload, \"pay_for_pizza\")\n\n# deduct cost from the correct wallet, using the Specification in the payload\n```\n\nBy not getting an exception, you know you can make the real calls to the backend application.\nThe RolesService will, just like in the other examples, return a Specification in the payload to be used in further processing.\nLike using the correct wallet for making a deduction.\n",
"bugtrack_url": null,
"license": null,
"summary": "Fractal Roles provides a flexible way to define fine-grained roles & permissions for users of your Python applications.",
"version": "1.0.7",
"project_urls": {
"Homepage": "https://github.com/douwevandermeij/fractal-roles"
},
"split_keywords": [],
"urls": [
{
"comment_text": null,
"digests": {
"blake2b_256": "c2a29454c8a01997c480e27197606db5771eacb5f5e1830ddd3173a6be81ff76",
"md5": "58ca4009636081ea2158901e0ae5910d",
"sha256": "ce08008ce15a5d4a7a61d9c2563deb71560841d5e628f68a126114d068022cd6"
},
"downloads": -1,
"filename": "fractal_roles-1.0.7-py3-none-any.whl",
"has_sig": false,
"md5_digest": "58ca4009636081ea2158901e0ae5910d",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.8",
"size": 7209,
"upload_time": "2024-06-05T10:34:24",
"upload_time_iso_8601": "2024-06-05T10:34:24.163667Z",
"url": "https://files.pythonhosted.org/packages/c2/a2/9454c8a01997c480e27197606db5771eacb5f5e1830ddd3173a6be81ff76/fractal_roles-1.0.7-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": null,
"digests": {
"blake2b_256": "df1979aa3291798d4d9ae01bba9077a525f9c06190d23d7451a717b0ddb01295",
"md5": "b5b7560b5e8e6de5f84a7a657c1762a8",
"sha256": "0e9a5d09f039ca959c68eeda5725db96d4d12c56d46b60c1e9c0b6ac1ca38079"
},
"downloads": -1,
"filename": "fractal_roles-1.0.7.tar.gz",
"has_sig": false,
"md5_digest": "b5b7560b5e8e6de5f84a7a657c1762a8",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.8",
"size": 14303,
"upload_time": "2024-06-05T10:34:25",
"upload_time_iso_8601": "2024-06-05T10:34:25.703728Z",
"url": "https://files.pythonhosted.org/packages/df/19/79aa3291798d4d9ae01bba9077a525f9c06190d23d7451a717b0ddb01295/fractal_roles-1.0.7.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-06-05 10:34:25",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "douwevandermeij",
"github_project": "fractal-roles",
"travis_ci": false,
"coveralls": true,
"github_actions": true,
"tox": true,
"lcname": "fractal-roles"
}