# django-topdownrbac

A Django library that implements hierarchical, top-down role-based access control (RBAC) with automatic permission propagation through object trees.

If you manage resources organized in a hierarchy (organizations, teams, projects, ...) and need a user with a role on a parent to automatically have that role on all descendants, this library handles it for you.

## Why this exists

Django's built-in permission system is flat: you grant permissions globally or per object, but there is no concept of hierarchy or propagation. No existing third-party package supports scoping permissions to specific object instances within a tree and propagating them downward.

django-topdownrbac fills that gap. Permissions are materialized into a dedicated table (`UserPermission`) for O(1) lookups at query time — no tree traversal or recursive joins needed. All changes to roles, bindings, group membership, or hierarchy automatically cascade to keep this table in sync.

**Trade-off: storage vs. speed.** Materialization means every user × permission × object combination is stored as a row. This makes permission checks and queryset filtering very fast, but the `UserPermission` table grows proportionally to the number of users, roles, and objects in your hierarchy. For small-to-medium projects this is negligible; for large-scale deployments with deep hierarchies, many roles, and thousands of users, monitor table size and plan your database capacity accordingly.

## Key concepts

| Concept | Model | Description |
|---|---|---|
| **Subject** | `UserSubject`, `UserGroupSubject` | A user or group of users. `UserSubject` extends Django's `AbstractUser`. |
| **Restricted Object** | `RestrictedObject` | Any object you want to protect. Organized as a tree (parent/children) using django-treebeard's Materialized Path. |
| **Role** | `Role` | A named collection of Django permissions. Can be marked immutable or externally provisioned (via YAML). |
| **Role Binding** | `RoleBinding` | Links a subject to a role on a specific restricted object. The `propagate` flag controls whether permissions apply to all descendants. |
| **User Permission** | `UserPermission` | A materialized row: one user, one permission, one object. This is the table queried at check time. |

![Concepts overview](docs/concepts-overview.png)

### How it works

1. Create a `RoleBinding` linking a subject + role + restricted object (optionally with `propagate=True`).
2. The library automatically bulk-creates `UserPermission` rows for every user x permission x object combination (including descendants when propagating).
3. Permission checks (`user.has_perm('view_thing', obj)`) hit the `UserPermission` table via a custom authentication backend -- no tree traversal at query time.
4. Any change (role update, binding deletion, group membership change, object moved in the tree) automatically cascades to keep `UserPermission` in sync.

## Example usage

```python
from topdownrbac.models import UserSubject, Role, RoleBinding, RestrictedObject
from django.contrib.auth.models import Permission

# Build a hierarchy
parent = RestrictedObject(parent=None); parent.save()
child = RestrictedObject(parent=parent); child.save()

# Create a role with a permission
viewer = Role.objects.create(name="Viewer")
viewer.add_permission(Permission.objects.get(codename="view_restrictedobject"))

# Bind the role to a user on the parent, propagating to all descendants
alice = UserSubject.objects.create_user(username="alice", password="secret")
RoleBinding.objects.create(
    role=viewer, subject=alice, restricted_object=parent, propagate=True
)

# alice can view both parent and child automatically
assert alice.has_perm("view_restrictedobject", parent)   # True
assert alice.has_perm("view_restrictedobject", child)    # True
```

## Installation
```bash
uv add django-topdownrbac
# or
pip install django-topdownrbac
```

For the full setup guide, custom model definitions, and DRF integration, see the [walkthrough](https://gobijacques.github.io/django-topdownrbac/latest/walkthrough/).
*You can also build the docs locally: `mkdocs serve`*

## Development

### Prerequisites
- [uv](https://docs.astral.sh/uv/) (Python package manager)
- [Task](https://taskfile.dev/) (task runner)
- Docker & Docker Compose (for PostgreSQL)

### Getting started

Install dependencies:
```bash
uv sync
```

Start Postgres and the dev server:
```bash
task dev
```

Full reset (wipe DB, rebuild, start fresh):
```bash
task fresh
```

### Documentation
MkDocs and its plugins (e.g. `mkdocs-puml`) are installed as dev dependencies by `uv sync`. To serve the docs locally:
```bash
task mkdocs
```

### Running tests
```bash
task test                        # run all tests
task test -- -k "not benchmark"  # skip benchmark tests, which can prove to be long
task test -- -v                  # verbose output
```

### Linting
```bash
task lint        # check with ruff
task lint:fix    # auto-fix issues
```

### Building Docker images
```bash
task build       # builds both CI and production images
task security    # build + Trivy vulnerability scan
```

