DUFT Security#

DUFT features a robust, declarative security model built on top of Django’s proven authentication and authorisation framework. Security is primarily enforced at the API level, ensuring that all interactions with DUFT Server are properly authenticated and authorised. The model is designed to be flexible and configurable, allowing implementers to define security policies without modifying the core application.

NOTE:  DUFT Security is enforced only at the API level, not within service logic. Service-level code is designed to operate independently of specific users, unless explicitly required. This ensures services remain decoupled from APIs, allowing them to be tested in isolation and even reused in other applications if needed. Services are never intended to be called directly outside the API context..

Key Features of DUFT Security#

  • Role-based access control (RBAC) at the API level – Users are assigned roles, which determine their access to different DUFT features.

  • Actions-based security – Instead of linking permissions directly to API endpoints, DUFT defines Actions, which represent user intents (e.g., RUN_DATA_TASK). Each Action has a predefined set of required permissions, keeping access control flexible and centralised.

  • Custom permissions – DUFT Server provides a set of predefined Django permissions (e.g., VIEW_DASHBOARD), but implementers can also define custom permissions in DUFT Config, which are dynamically loaded into Django’s security system.

  • Default users and roles – DUFT ships with default roles (e.g., data_manager) with predefined permissions, allowing quick setup while remaining configurable.

Key Engineering Decisions: DUFT Security

Several key engineering decisions shaped the design and implementation of DUFT Security:

  • Declarative security model – Instead of embedding security checks inside API views, APIs are “decorated” with security annotations that enforce permissions at runtime. This keeps API logic clean and ensures a consistent approach.

  • Action-based security model – Permissions are assigned to Actions, representing “intents” rather than specific API endpoints. This abstraction allows for greater flexibility in defining security rules.

  • Centralised Permission Service – A dedicated Permission Service serves as the single source of truth for authorisation, determining whether a user has the required permissions for a given Action. While currently role-based, it is designed to support object-level (record-based) permissions in future updates.

  • Extensibility with DUFT Config – Implementers can define custom permissions in DUFT Config without modifying the core system. Permissions are dynamically loaded at startup.

  • Thorough unit testing – DUFT security is validated through extensive test coverage. A dedicated dummy API is included specifically for security testing, using parametrised test cases to verify different access scenarios.

Actions, Roles and Permissions#

DUFT’s security model is structured around Actions, which represent high-level user intents rather than specific API endpoints. Each Action has a set of permissions associated with it, and these permissions determine whether a user can perform that action.

Actions and Permission Mapping#

An Action is defined as an operation that a user can perform, such as running a data task or viewing a dashboard. Each Action has an associated set of permissions that must be granted to a user for the Action to be executed.

# Defines an “intent” that can be done by the user
class Actions:
    RUN_DATA_TASK = "run_data_task"
    VIEW_DASHBOARD = "view_dashboard"

# Defines which (Django) permissions are required for 
# each Action
ACTION_PERMISSIONS = {
    Actions.RUN_DATA_TASK: ["execute_data_task"],
    Actions.VIEW_DASHBOARD: ["view_dashboard"],
}

Role-Based Access Control#

Users in DUFT are assigned roles, and roles grant specific permissions. For example, a data_manager role might have permissions to execute data tasks but not to modify dashboards.

# Django-based roles with required permissions

ROLE_PERMISSIONS = {
  "data_manager": ["execute_data_task", 
                   view_dashboard"],
  "admin": ["execute_data_task", 
            "view_dashboard", 
            "modify_dashboard"],
}

Roles and their associated permissions are checked centrally in the Permission Service, ensuring all security decisions are managed from a single place.

Default Users and Roles#

To simplify deployment, DUFT ships with predefined default users and roles. These users provide common access patterns while remaining fully configurable.

Default Roles#

DUFT provides the following default roles:

  • admin – Has full control over all features.

  • data_manager – Can execute data tasks and view dashboards.

  • viewer – Can only view dashboards, without modifying anything.

These roles are defined in DUFT Config and automatically loaded into Django’s security system during system startup. Implementers can customise or disable default users by modifying startup.py in DUFT Config.

Custom Permissions in DUFT Config#

DUFT allows implementers to define custom permissions in DUFT Config without modifying the core application. These permissions are dynamically loaded into Django’s security system at runtime.

Implementers can define additional permissions in startup.py within DUFT Config, for example:

# Django-based permissions to be loaded into Auth at
# startup when not yet present

CUSTOM_PERMISSIONS = [
    {"codename": "approve_reports", "name": "Can approve reports"},
    {"codename": "export_data", "name": "Can export data"},
]

These custom permissions will be loaded into Django’s security database (Auth) and can then be assigned to DUFT objects, such as dashboards and Data Tasks within DUFT Config, and users can configure additional roles through the Django admin interface. In the near future, implementers will also be able to declaratively create additional roles based on the custom permissions.

The Token API#

DUFT secures API access using JSON Web Tokens (JWT), ensuring all requests are authenticated without relying on traditional session-based authentication. Users obtain an access token by providing their credentials to /api/token/, which must be included in API requests as a Bearer token. Since access tokens expire, a refresh token is issued alongside it, allowing users to obtain a new access token without logging in again by calling /api/token/refresh/.

The authentication system is built on Django Rest Framework SimpleJWT, making it secure, stateless, and easily extendable. DUFT also provides a /api/token/verify/ endpoint to validate tokens, ensuring their integrity before granting access. This token system seamlessly integrates with DuftHttpClient (see below),  which automatically refreshes tokens when needed, providing a seamless experience for authenticated users.

The Permission Service: A Single Source of Truth#

The Permission Service is the authorisation engine for DUFT, ensuring all access control checks are centralised, consistent, and extensible. Instead of scattering permission checks throughout the system, all authorisation decisions are centralised in this service. This keeps API logic clean and ensures consistent security enforcement across all DUFT features.

Key Responsibilities#

  • Centralised Authorisation – The Permission Service determines whether a user has the necessary permissions for an Action, making it the single source of truth for security checks.

  • Action-Based Validation – When a request is made to an API endpoint, the Permission Service checks the required permissions for the corresponding Action and verifies whether the user’s assigned role grants them access.

  • Future Support for Object-Level Security – While permissions are currently role-based, future updates will introduce object-level security, allowing record-specific access control..

Here is an example for checking permissions by using the permission_required decorator, which takes an Action as a parameter:

# All permissions are based on Actions, so:  
# - Each API is mapped to an Action

# - Each Action defines the required permissions

from rest_framework.views import APIView
from rest_framework.response import Response
from api.permissions import permission_required

class RunDataTaskView(APIView):
    @permission_required(Actions.RUN_DATA_TASK)
    def post(self, request):
        return Response({"message": "Data Task executed successfully"})

Security Fixtures and Testing#

Security in DUFT is thoroughly unit tested to ensure all API endpoints enforce authentication and authorisation correctly.

Parametrised Security Tests#

DUFT includes a dedicated security API for testing, allowing security rules to be validated systematically, for example:

# Simplified version of a parametrised test
@pytest.mark.parametrize("user_role, expected_status", [
    ("admin", 200),
    ("data_manager", 403),
    ("viewer", 403),

])

def test_security_restricts_access(client, user_role, expected_status):
    client.login(username=user_role, password="testpassword")
    response = client.get("/api/secure-endpoint/")
    assert response.status_code == expected_status

A dummy security API exists purely for testing purposes, helping verify that permission checks function as expected.

# This API does nothing, other than supporting
# unit tests in harnessing the system

from rest_framework.views import APIView
from rest_framework.response import Response
from api.permissions import permission_required

class SecureTestView(APIView):
    @permission_required("export_data")
    def get(self, request):
        return Response({"message": "Access granted”})`