Metadata-Version: 2.4
Name: drf-cedar
Version: 0.1.1
Summary: Cedar authorization for Django REST Framework apps
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.14
Requires-Dist: cedarpy>=4.8.0
Requires-Dist: djangorestframework>=3.16.1
Requires-Dist: pillow>=12.1.1
Description-Content-Type: text/markdown

# drf-cedar

`drf-cedar` is a lightweight Django REST framework for **policy-first authorization** with [Cedar](https://www.cedarpolicy.com/).
It is designed so route handlers stay small while authorization logic stays centralized in policy files.

---

## What drf-cedar gives you

- Enforced Cedar checks on all CRUD endpoints built from Crucible views.
- Consistent action naming (`ListVehicles`, `CreateBooking`, `AdminGetProduct`, etc.).
- Scoped factory views for nested routes like `/vehicles/{vehicle}/bookings/`, enabling policy decisions on parent resources during `list` and `create`.
- Model-to-Cedar entity mapping using `authz_fields()` so policy context can be expressed in Cedar instead of view code.
- Optional principal attribute providers for injecting extra principal attributes (for example, council membership).
- Transaction-safe lifecycle hooks (`post_create`, `post_commit_create`, etc.) for side effects.

---

## Core Concepts

### 1) Policy-first route authorization

Every HTTP operation on `drf-cedar` base views is mapped to an action and authorized before business logic runs.

- `GET /resource/{pk}` -> `Get<Resource>`
- `GET /resource/` -> `List<Resources>`
- `POST /resource/` -> `Create<Resource>`
- `PUT/PATCH /resource/{pk}` -> `Update<Resource>`
- `DELETE /resource/{pk}` -> `Delete<Resource>`

Action names are generated from:

`{action_prefix}{Verb}{ModelNameOrPlural}`

Where:
- `action_prefix` defaults to `""` (can be overridden, for example `"Admin"`).
- Model name comes from `queryset.model._meta.verbose_name` (or plural form for list).

### 2) Resource selection for authorization

`drf-cedar` authorizes against:

- Object endpoints: the target object from `get_object()`.
- List/create endpoints: `get_scope_resource()` (if provided), else system-global resource `System::"global"`.

This is what makes scoped nested routes powerful for policy checks.

### 3) Entity attribute projection

Models can define:

```python
def authz_fields(self) -> dict:
    ...
```

Those fields become Cedar entity attributes and can be used in policy conditions.

### 4) Principal enrichment

Principal attributes can be injected by pluggable providers listed in settings, letting policies refer to derived/related attributes on the principal.

---

## Components

### `drf_cedar.authz`

- `Authz.authorize(user, action, resource)`
  - Builds principal/entity graph.
  - Builds Cedar request and calls `cedarpy.is_authorized(...)`.
  - Raises DRF `PermissionDenied("Forbidden")` when denied.
- Supports anonymous principal as `Anonymous::"guest"`.
- Supports user groups as Cedar parents (`Group::<name>`).

### `drf_cedar.views`

- `BaseCedarAuthzView`
  - Handles authz for HTTP methods.
  - Loads policy file from settings.
  - Loads principal attribute providers from settings.
- `ModelCrudView`
  - Full CRUD + list with authorization.
- `ReadOnlyModelView`
  - Retrieve + list with authorization.
- `ScopedViewMixin`
  - For nested resources:
    - Filters queryset by parent scope.
    - Binds parent object on create.
    - Uses parent object as authz resource for list/create.
- Transaction-aware CRUD mixins:
  - `CreateModelMixin`, `UpdateModelMixin`, `DestroyModelMixin`
  - Hooks: `post_*` and `post_commit_*`.

### `drf_cedar.serializers`

- `BaseModelSerializer`
  - Standard read-only timestamps and id fields.
  - `_get_attr(...)` helper for create/update validations.
- `DataURLImageField`
  - Accepts multipart file or data URL (`data:image/png;base64,...`).

### `drf_cedar.models`

- `Entity` abstract model with `created_at` / `updated_at`.
- `money(...)` helper for decimal quantization.


---

## Settings Contract

```python
CEDAR_POLICY_PATH = "path/to/policies.cedar"
CEDAR_PRINCIPAL_ATTRIBUTE_PROVIDERS = [
    "myapp.authz.SomePrincipalAttrProvider",
]
```

Provider contract:

```python
class SomePrincipalAttrProvider:
    def get_attributes(self, user) -> dict:
        return {}
```

Example:

```python
class CouncilUserAttrProvider:
    def get_attributes(self, user):
        if hasattr(user, "council_user"):
            return {"council": {"id": str(user.council_user.council.id)}}
        return {}
```

---

## Usage Patterns

## 1) Standard CRUD endpoint with policy enforcement

```python
class CouncilCrudView(ModelCrudView):
    serializer_class = CouncilSerializer
    queryset = Council.objects.all().order_by("name")
```

No explicit permission code required. Actions become:
- `ListCouncils`, `GetCouncil`, `CreateCouncil`, `UpdateCouncil`, `DeleteCouncil`

## 2) Read-only endpoint

```python
class CustomerProductView(
    RetrieveModelMixin,
    ListModelMixin,
    BaseCedarAuthzView,
):
    serializer_class = ProductSerializer
    queryset = Product.objects.all().order_by("id")
```

## 3) Action prefix for role-segmented policy namespace

```python
class AdminProductCrudView(ModelCrudView):
    action_prefix = "Admin"
    serializer_class = AdminProductSerializer
    queryset = Product.objects.all().order_by("id")
```

This yields actions like `AdminListProducts`, `AdminGetProduct`, etc.

## 4) Scoped factory view for nested list/create

```python
class VehiclesForUserView(
    ScopedViewMixin, CreateModelMixin, ListModelMixin, BaseCedarAuthzView
):
    serializer_class = VehicleSerializer
    queryset = Vehicle.objects.all().order_by("-created_at")
    scope_model = User
    scope_kwarg = "owner"
```

With route `/users/{owner}/vehicles/`:
- `GET` authorizes `ListVehicles` **against `User::{owner}`**
- `POST` authorizes `CreateVehicle` **against `User::{owner}`**

The created `Vehicle.owner` is auto-bound from URL scope.

## 5) Scoped list where query path differs from URL kwarg

```python
class BookingsForCouncilView(ScopedViewMixin, ListModelMixin, BaseCedarAuthzView):
    serializer_class = BookingSerializer
    queryset = Booking.objects.all().order_by("-created_at")
    scope_model = Council
    scope_kwarg = "council"
    scope_attr = "vehicle__council"
```

`scope_attr` allows filtering nested relations while still authorizing on `Council`.

## 6) Custom endpoint action outside CRUD convention

```python
class UpdateInfoView(UpdateModelMixin, BaseCedarAuthzView):
    serializer_class = UserSerializer
    queryset = User.objects

    def patch(self, request, *args, **kwargs):
        resource = self.get_object()
        self.authz.authorize(request.user, "UpdateUserInfo", resource)
        return self.partial_update(request, *args, **kwargs)
```

Use this pattern when action naming must be explicit or domain-specific.

## 7) Model attributes for policy conditions

`Vehicle.authz_fields()`:

```python
def authz_fields(self):
    return {"owner": {"id": str(self.owner.id)}}
```

`Booking.authz_fields()`:

```python
def authz_fields(self):
    return {
        "vehicle": {
            "owner": {"id": str(self.vehicle.owner.id)},
            "council": {"id": str(self.vehicle.council.id)},
        },
        "installer": {"user": {"id": str(self.installer.user.id)}},
    }
```

These fields drive Cedar conditions like owner/council matching.

## 8) Post-save hooks with transaction semantics

Crucible mixins support:
- `post_create`, `post_update`, `post_destroy` (inside transaction)
- `post_commit_create`, `post_commit_update`, `post_commit_destroy` (after commit)


---

## Cedar policy examples

From this project's policy file:

- Admin full access:
  - `principal in Group::"Admins"` permits all actions/resources.
- Driver-owned vehicle access:
  - Permit `GetVehicle|UpdateVehicle|DeleteVehicle|CreateBooking` when
    `resource.owner.id == principal.id`.
- Scoped list for council users:
  - Permit `ListBookings` on `Council` when
    `resource.id == principal.council.id`.

This demonstrates why scoped routes matter: list/create can be decided by parent resource attributes without special-case Python checks.

---

## Quick Start

1. Install `drf-cedar`
2. Add settings:
   - `CEDAR_POLICY_PATH`
   - optional `CEDAR_PRINCIPAL_ATTRIBUTE_PROVIDERS`
3. Define Cedar policy actions to match your view action naming.
4. Build views from `ModelCrudView` / `ReadOnlyModelView` / mixin composition.
5. Add `authz_fields()` to domain models referenced in policy conditions.
6. Prefer scoped nested routes for create/list operations where parent context matters.

---

## AI Agent Implementation Checklist

When adding a new endpoint:

1. Choose base class:
   - CRUD: `ModelCrudView`
   - Read-only: `ReadOnlyModelView` or retrieve/list mixins + `BaseCedarAuthzView`
   - Nested scope: include `ScopedViewMixin`
2. Set `queryset` and `serializer_class`.
3. For scoped views set:
   - `scope_model`
   - `scope_kwarg`
   - optional `scope_attr` for relation traversal
4. Confirm generated Cedar action names; set `action_prefix` if needed.
5. Add/adjust Cedar policies for those actions.
6. Add `authz_fields()` to models if policy conditions need related IDs.
7. For derived principal context, add a provider class and register it in settings.
8. Keep business logic in serializer hooks / `post_*` hooks; keep auth decisions in Cedar.

---

## Security Notes

- Deny-by-default: any missing policy/action mapping returns `403 Forbidden`.
- Avoid bypassing `BaseCedarAuthzView`.
- Keep policy file under version control and code review changes like code.
- Treat `authz_fields()` as part of your security boundary; changes can alter access.
