initial upload to register the name for future development
Project description
SAPL Integration library for Django
This library provides decorators to enforce methods, functions, asyncGenerator and AsyncWebsocketConsumer with SAPL functionality, allowing you to implement attribute streaming based access control (ABAC) in a Django application.
If you're not familiar with ABAC, it's recommended that you first read the SAPL documentation to gain a better understanding of the underlying concepts.
What is SAPL
SAPL is an evolution of ABAC that uses a publish/subscribe model instead of a request-response model. When a client sends an Authorization Subscription to the Policy Decision Point (PDP), it receives a stream of Decisions that can update the decision for the given Subscription for the client. The SAPL_Django library handles the functionality of updating and enforcing newly received Decisions in the background, allowing developers to focus on writing policies. A comprehensive SAPL documentation can be found on https://sapl.io, including a playground for writing policies.
Demo Application
A public demo project is available, which shows how SAPL and the SAPL Django integration library is used in a Django Application
The demo project can be found on GitHub here
how to install
The SAPL Django integration library is available on PyPI and can be installed using pip
.
To install SAPL_Django run pip install sapl_django
.
initialize the library
To use SAPL_Django, you need to install it as an application in your Django project by adding it to the INSTALLED_APPS
list in your project's settings file, as shown in the example below. It's recommended to add it at the end of the list
to ensure that other packages are already initialized.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'sapl_django',
]
SAPL_Django requires access to the Request object that the server receives. To make this Request available to the
library, you need to install a middleware that saves the Request in a ContextVar
. You should add this middleware to the
MIDDLEWARE
list in your project's settings file. It's important to add it at the end to ensure that it has access to all
the previous middleware. An example of how to add the SAPL_Django middleware to the MIDDLEWARE
list is shown below.
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'sapl_django.sapl_request_middleware.SAPLMiddleware',
]
How to configure SAPL_Django
The configuration is used to determine, how to connect to the PDP. This PDP should be a SAPL server, for which a documented open source version is available on GitHub.
The configuration can be made by adding a "POLICY_DECISION_POINT"
key to the settings of the project.
The default configuration looks like this:
POLICY_DECISION_POINT = {
"dummy" : False,
"debug": False,
"base_url": "http://localhost:8080/api/pdp/",
"key": "YJidgyT2mfdkbmL",
"secret": "Fa4zvYQdiwHZVXh",
"verify": False,
"backoff_const_max_time": 3,
"backoff_expo_max_value": 50
}
- dummy: Enables a dummy PDP, which is used instead of a remote PDP. This PDP always grants access and should never be used in production.
- debug: Enables debugging , which adds logging.
- base_url: The URL, where your PDP Server is located. This has to include the path
'/api/pdp/'
of the PDP Server. - key: Access to the API of the SAPL PDP Server requires "Basic Auth". The client key (username) can be set with this parameter. The default is the default implemented credentials of a SAPL-Server-lt
- secret: The password which is used to get access to the API.
- verify:
- backoff_const_max_time: When an error occurs, while requesting a single Decision from the PDP, SAPL_Django does a retry. This parameter determines, how many seconds the library should retry to connect, before it aborts and denies the access.
- backoff_expo_max_value: When an error occurs, while requesting a stream of Decision from the PDP, SAPL_Django does a retry with increasing intervalls between each retry. This parameter determines, how much seconds is the maximum time in seconds between each retry.
How to use it
To secure a Django project with SAPL, developers can decorate the functions, methods, asyncGenerator and AsyncWebsocketConsumer that they want to enforce with SAPL decorators. The SAPL Django integration library is designed to be compatible with both ASGI and WSGI servers, and can recognize both synchronous and asynchronous functions automatically.
The library provides decorators for functions, methods, AsyncWebsocketConsumer and templates, which are explained in their respective chapters in the documentation. The decorators can be customized with arguments for the Subject, Action, Resource, and Environment parameters. If no arguments are provided, the library uses default implementations to determine these values and create an AuthorizationSubscription, which is sent as a JSON to the PDP to request a decision.
An AuthorizationSubscription as JSON with default implementation for a method or function will look like this:
{
"subject": {
"user_id": "id of the requesting user",
"username": "name of the requesting user",
"first_name": "first name",
"last_name": "last name",
"is_active": "is the user still active",
"is_superuser": "is the requesting user a superuser",
"permissions": "list of permissions this user has",
"groups": "list of groups to which the user belongs",
"last_login": "time, when the user logged in the last time",
"is_authenticated": "has the user been authenticated",
"authorization": "JWT token, which was used for the request"
},
"action": {
"function": {
"function_name": "name of the function",
"class": "name of the class, to which the decorated method belongs",
"type": "type of the class in which the method is decorated"
},
"request": {
"path": "request.path",
"method": "request.method",
"view_name": "resolver.view_name",
"route": "resolver.route",
"url_name": "resolver.urlname"
}
},
"resources": {
"function": {
"url_kwargs": "arguments which were provided in the request",
"kwargs": "arguments with which the decorated function was called"
},
"request": {
"GET" : "Arguments of the GET Request",
"POST" : "Arguments of the POST Request"
},
"return_value": "Return value of the decorated function"
}
}
To determine access control in a system, policies must be defined. SAPL provides a policy language that allows you to write policies that can be evaluated by the SAPL Server. The policies specify who should have access to what resources and under what conditions.
When a policy is evaluated, it produces a decision, which is either "PERMIT" or "DENY". A decision can also contain obligations or advices. Obligations are things that must be done if access is granted, while advices are suggestions for things that could be done if access is granted.
The SAPL documentation provides more information on how to write policies and how the SAPL Server evaluates Authorization Subscriptions. Additionally, there is a section on Obligations and Advices that provides more information on how they work.
Decorators for functions and methods
Functions and methods can be decorated with one 3 possible Decorators, which are:
pre_enforce
when a function shall be enforced, before it is called.post_enforce
when the function is already executed and the return_value is needed to determine if the requesting client has access to these data.pre_and_post_enforce
when the function is enforced before and after it is called. This decorator can be used, when the return value of the function is needed to finally decide if access can be granted, but the execution of the function is expensive and there are certain parameters of a request, which can already be used to deny access before the return value is known.
an example for a pre_enforced function would be:
from sapl_base.decorators import pre_enforce
@pre_enforce
def pre_enforced_function(*args,**kwargs):
return_value = "Do something"
return return_value
The AuthorizationSubscription which could be sent to the PDP to request a Decision could be:
{
"subject":{
"user_id":4,"username":"alina","first_name":"Alina","last_name":"Aurich","is_active":true,"is_superuser":false,
"permissions":["medical.add_patient","medical.change_patient","medical.view_patient"],
"groups":["Head Nurse","Nurse"],"last_login":"2023-06-12 10:15:51.149613+00:00","is_authenticated":true
},
"action":{
"request":{
"path":"/patients/5/update_patient_data/","method":"GET","view_name":"medical:update_patient",
"route":"patients/<int:pk>/update_patient_data/","url_name":"update_patient"
},
"function":{
"class":"PatientManager","function_name":"find_patient_by_pk","type":"Manager"
}
},
"resource":{
"return_value":{
"id":5,"name":"hello","icd11_code":"5","diagnosis_text":"data",
"attending_doctor":"doctor","room_number":20,"is_related_to_staff":false
},"function":{
"url_kwargs":{
"pk":5
},"kwargs":{
"patient_pk":5
}
}
}
}
Instead of using the default implementation to determine Subject,Action,Resources and Environment it is possible to provide Arguments for these parameter. An explanation on how to provide Arguments for decorators is explained in the chapter Providing arguments to a decorator
Decorators for AsyncGenerator and AsyncWebsockets
Django provides a class AsyncWebsocketConsumer, which can be enforced with SAPL. There are 3 possible decorators which can be used to enforce AsyncGenerator and AsyncWebsocketConsumer:
enforce_till_denied
enforce_drop_while_denied
enforce_recoverable_if_denied
When an AsyncWebsocketConsumer is decorated, these decorators will check for Permission, whenever data should be sent and can prevent the sending of data. Using Obligations and Advices it is also possible to restrict the usage of certain methods within the class. The Decorator also add methods to send an error message on a Recoverable_if_denied, or it can close the Connection on a pre_enforce decorator Decorating doesn't prohibit the usage of sapl decorators for methods in the class.
When an AsyncGenerator is decorated, the decorator will enforce each yield of the AsyncGenerator. The AsyncGenerator can be closed when the Permission is denied, or it can just drop a value, which should be yielded. Additionally, any value can be handled with Constrainthandlers before yielding it.
Decorators for templates
To use SAPL in a template, there are three available tags provided by the SAPL Django integration library:
enforce
: Begins a node that will be enforced with SAPL. This tag can accept arguments.endenforce
: Ends the node, that will be enforced with SAPL.deny
: Begins a node inside an Enforce node that will be displayed when the permission is denied.
Each node in a template that is enclosed by an enforce and endenforce Tag creates an AuthorizationSubscription and requests a Decision from the PDP
If the decision of a node is PERMIT, the content until a 'deny' or 'endenforce' Tag will be displayed, while the parts after a 'deny' Tag won't be displayed. If the received decision is 'DENY' only the content after a 'deny' Tag will be displayed.
Providing arguments to a decorator
The decorators provided by the SAPL Django integration library allow for customization with arguments for the four parameters: Subject, Action, Resource, and Environment. These arguments replace the default implementation for determining the value of the parameter.
While the first three parameters have default implementations, the Environment parameter can only be set, when an argument to determine its value is provided.
Arguments for the decorators can be any value that can be converted to JSON, or a function that returns a JSON-serializable value. If a function is provided, it will be called when an AuthorizationSubscription is created, and the return value of the function will be used as the value for the corresponding parameter in the AuthorizationSubscription.
An example for a pre_enforce Decorator with a string as action and a function as Environment would be:
TODO: EXAMPLE
Obligations and Advices
After receiving a decision from the PDP (Policy Decision Point), constraints may be attached to it, which can be categorized as either obligations or advices. Obligations must be fulfilled; otherwise, the permission will be denied. Advices, on the other hand, are recommended to be fulfilled, but they won't affect the decision even if they're not.
To handle these constraints, the library provides an abstract class called ConstraintHandlerProvider
, which can manage
them. Developers must create classes to handle the constraints and register them with the library to ensure that the
library can check if the given constraints can be managed.
How to create ConstraintHandlerProvider
In order to create ConstraintHandlerProvider, 4 abstract ConstraintHandlerProvider are available to inherit from. These abstract classes are:
ErrorConstraintHandlerProvider
OnDecisionConstraintHandlerProvider
FunctionArgumentsConstraintHandlerProvider
ResultConstraintHandlerProvider
All of these classes inherit from the ConstraintHandlerProvider
base class, which defines the methods that must be
implemented in order to handle constraints.
The Baseclass is defined like this:
from abc import abstractmethod, ABC
class ConstraintHandlerProvider(ABC):
@abstractmethod
def priority(self) -> int:
"""
ConstraintHandlerProvider are sorted by the value of the priority, when the ConstraintHandlerBundle is created
:return: value by which ConstraintHandlerProvider are sorted
"""
return 0
@abstractmethod
def is_responsible(self, constraint) -> bool:
"""
Determine if this ConstraintHandler is responsible for the provided constraint
:param constraint: A constraint, which can be an Obligation or an Advice, for which the
ConstraintHandlerProvider checks if it is responsible to handle it.
:return: Is this ConstraintHandlerProvider responsible to handle the provided constraint
"""
pass
@abstractmethod
def handle(self, argument):
"""
Abstractmethod, which needs to be implemented by a ConstraintHandlerProvider
:param argument: The argument, which is provided to the ConstraintHandler, when it is called. This argument can
be an Exception, function, decision, or the result of the executed function.
"""
When a decision contains a constraint, the library checks all registered ConstraintHandlerProviders
to see if their
is_responsible
method evaluates to True
for the given constraint. The responsible ConstraintHandlerProvider
are
gathered and sorted based on the value returned by their priority
method. Finally, the handle
methods of the sorted list
of ConstraintHandlerProviders
that are responsible for the given constraint are executed in the order of the list.
An example for a ConstraintHandlerProvider
, which logs the received decision when it contains a constraint which
equals "log decision" would be:
from sapl_base.decision import Decision
from sapl_base.constraint_handling.constraint_handler_provider import OnDecisionConstraintHandlerProvider
import logging
class LogNewDecisionConstraintHandler(OnDecisionConstraintHandlerProvider):
def handle(self, decision: Decision) -> None:
logging.info(str(decision))
def priority(self) -> int:
return 0
def is_responsible(self, constraint) -> bool:
return True if constraint == "log decision" else False
How to register ConstraintHandlerProvider
The ConstraintHandlerService
class manages constraints and includes a singleton
(i.e., constraint_handler_service
)
that's created upon file loading. All registered ConstraintHandlerProviders
are taken into account by the singleton
when
checking if decision constraints can be handled.
The ConstraintHandlerService
class provides methods to register a single ConstraintHandlerProvider
or a list of
ConstraintHandlerProviders
for each of the four types of ConstraintHandlerProvider
.
A recommended way to register a ConstraintHandlerProvider
is to create a package that contains all the necessary
ConstraintHandlerProviders
and to register them in the __init__
file of the package. For example, you can create a file
that contains all OnDecisionConstraintHandlerProviders
and add the ConstraintHandlerProvider
created in the previous chapter.
Then, in the __init__
file of the package, you can register the LogNewDecisionConstraintHandler
at the OnDecisionConstraintHandler
:
from sapl_base.constraint_handling.constraint_handler_service import constraint_handler_service
from . import on_decision_constraint_handler_provider as on_decisions
constraint_handler_service.register_on_decision_constraint_handler_provider(on_decisions.LogNewDecisionConstraintHandler())
You can install the created package in the settings of your Django project to register all ConstraintHandlerProviders
when the app starts. If you can't install the package in the settings due to an AppRegistryNotReady
exception, you can
initialize and register the ConstraintHandlerProvider
when the app is ready.
The Demo Project for SAPL_Django initializes the
ConstraintHandlerService
when the app is ready. The following code of the demo project imports the package of the
ConstraintHandler
when the app is ready:
from django.apps import AppConfig
class ChatConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'medical'
def ready(self):
"""
The method ready configure if the application or DB first time runs.
It initializes the DB and add the demoData and register the constraintService
"""
if "runserver" not in sys.argv:
return True
# import here to avoid AppRegistryNotReady exception
from medical.demo_data import initialize_database
initialize_database()
import djangoDemo.medical.constraint_handler_provider
How to migrate
To integrate SAPL with an existing Django project, you can follow these steps: install, initialize, and configure SAPL in the existing project as if it were a new project.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for SAPL_Django-0.3.6-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | f14cdb7b793eadf95e9f67a8425a69554c0077dd65201da648cfb3de2e671abd |
|
MD5 | 139537e2234c891627d88e6222593f61 |
|
BLAKE2b-256 | b92b06947f017bc5681192d50a07f03c179d60f710b5a2a5b686707a21e250de |