# Entitlements System

The entitlements system provides a pluggable backend architecture for
checking whether a user is allowed to access the application. It
integrates with the DeployCenter API in production and uses a local
backend for development.

Calendars checks two entitlements:
- `can_access`: whether the user can use the app at all
- `can_admin`: whether the user is an admin of their organization
  (e.g. can create/delete resources)

## Architecture

```
┌─────────────────────────────────────────────┐
│       OIDC Authentication Backend           │
│  post_get_or_create_user() — warms cache    │
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│          UserMeSerializer                   │
│  GET /users/me/ → { can_access, can_admin } │
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│            Service Layer                    │
│       get_user_entitlements()               │
└──────────────┬──────────────────────────────┘
               │
┌──────────────▼──────────────────────────────┐
│       Backend Factory (singleton)           │
│       get_entitlements_backend()            │
└──────────────┬──────────────────────────────┘
               │
       ┌───────┴───────┐
       │               │
┌──────▼─────┐  ┌──────▼───────────────┐
│   Local    │  │    DeployCenter      │
│  Backend   │  │      Backend         │
│ (dev/test) │  │ (production, cached) │
└────────────┘  └──────────────────────┘
```

### Components

- **Service layer** (`core/entitlements/__init__.py`): Public
  `get_user_entitlements()` function and
  `EntitlementsUnavailableError` exception.
- **Backend factory** (`core/entitlements/factory.py`):
  `@functools.cache` singleton that imports and instantiates the
  configured backend class.
- **Abstract base** (`core/entitlements/backends/base.py`): Defines
  the `EntitlementsBackend` interface.
- **Local backend** (`core/entitlements/backends/local.py`): Always
  grants access. Used for local development.
- **DeployCenter backend**
  (`core/entitlements/backends/deploycenter.py`): Calls the
  DeployCenter API with Django cache and stale fallback.

### Integration points

1. **OIDC login** (`core/authentication/backends.py`):
   `post_get_or_create_user()` calls `get_user_entitlements()` with
   `force_refresh=True` to warm the cache. Login always succeeds
   regardless of `can_access` value — access is gated at API level
   and in the frontend.
2. **User API** (`core/api/serializers.py`): `UserMeSerializer`
   exposes `can_access` as a field on `GET /users/me/`. Fail-open:
   returns `True` when entitlements are unavailable.
3. **Default calendar creation** (`core/signals.py`):
   `provision_default_calendar` checks entitlements before creating a
   calendar for a new user. Fail-closed: skips creation when
   entitlements are unavailable.
4. **CalDAV proxy** (`core/api/viewsets_caldav.py`): Blocks
   `MKCALENDAR` and `MKCOL` methods for non-entitled users.
   Other methods (PROPFIND, REPORT, GET, PUT, DELETE) are allowed
   so that users invited to shared calendars can still use them.
   Fail-closed: denies creation when entitlements are unavailable.
5. **Import events** (`core/api/viewsets.py`): Blocks
   `POST /calendars/import-events/` for non-entitled users.
   Fail-closed: denies import when entitlements are unavailable.
6. **Frontend** (`pages/index.tsx`, `pages/calendar.tsx`): Checks
   `user.can_access` and redirects to `/no-access` when `false`.
   Calendar creation uses MKCALENDAR via CalDAV proxy (no Django
   endpoint).

### Error handling

- **Login is fail-open**: if the entitlements service is unavailable,
  login succeeds and the cache warming is skipped.
- **User API is fail-open**: if the entitlements service is
  unavailable, `can_access` defaults to `True`.
- **Calendar creation is fail-closed**: if the entitlements service
  is unavailable, the default calendar is not created (avoids
  provisioning resources for users who may not be entitled).
- **CalDAV proxy MKCALENDAR/MKCOL is fail-closed**: if the
  entitlements service is unavailable, calendar creation via CalDAV
  is denied (returns 403).
- **Import events is fail-closed**: if the entitlements service is
  unavailable, ICS import is denied (returns 403).
- **Resource provisioning is fail-closed**: if the entitlements
  service is unavailable, resource creation/deletion is denied
  (returns 403). The `can_admin` check follows the same pattern
  as `can_access` fail-closed checks.
- The DeployCenter backend falls back to stale cached data when the
  API is unavailable.
- `EntitlementsUnavailableError` is only raised when the API fails
  **and** no cache exists.

## Configuration

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `ENTITLEMENTS_BACKEND` | `core.entitlements.backends.local.LocalEntitlementsBackend` | Python import path of the backend class |
| `ENTITLEMENTS_BACKEND_PARAMETERS` | `{}` | JSON object passed to the backend constructor |
| `ENTITLEMENTS_CACHE_TIMEOUT` | `300` | Cache TTL in seconds |

### DeployCenter backend parameters

When using
`core.entitlements.backends.deploycenter.DeployCenterEntitlementsBackend`,
provide these in `ENTITLEMENTS_BACKEND_PARAMETERS`:

```json
{
  "base_url": "https://deploycenter.example.com/api/v1.0/entitlements/",
  "service_id": "calendar",
  "api_key": "your-api-key",
  "timeout": 10,
  "oidc_claims": ["siret"]
}
```

| Parameter | Required | Description |
|---|---|---|
| `base_url` | Yes | Full URL of the DeployCenter entitlements endpoint |
| `service_id` | Yes | Service identifier in DeployCenter |
| `api_key` | Yes | API key for `X-Service-Auth: Bearer` header |
| `timeout` | No | HTTP timeout in seconds (default: 10) |
| `oidc_claims` | No | OIDC claim names to forward as query params |

### Example production configuration

```bash
ENTITLEMENTS_BACKEND=core.entitlements.backends.deploycenter.DeployCenterEntitlementsBackend
ENTITLEMENTS_BACKEND_PARAMETERS='{"base_url":"https://deploycenter.example.com/api/v1.0/entitlements/","service_id":"calendar","api_key":"secret","timeout":10,"oidc_claims":["siret"]}'
ENTITLEMENTS_CACHE_TIMEOUT=300
```

## Backend interface

Custom backends must extend `EntitlementsBackend` and implement:

```python
class MyBackend(EntitlementsBackend):
    def __init__(self, **kwargs):
        # Receives ENTITLEMENTS_BACKEND_PARAMETERS as kwargs
        pass

    def get_user_entitlements(
        self, user_sub, user_email, user_info=None, force_refresh=False
    ):
        # Return: {"can_access": bool, "can_admin": bool, ...}
        # Raise EntitlementsUnavailableError on failure.
        pass
```

## DeployCenter API

The DeployCenter backend calls:

```
GET {base_url}?service_id=X&account_type=user&account_email=X
```

Headers: `X-Service-Auth: Bearer {api_key}`

Query parameters include any configured `oidc_claims` extracted from
the OIDC user_info response (e.g. `siret`).

Expected response:
`{"entitlements": {"can_access": true, "can_admin": false}}`

## Access control flow

The entitlements check follows a two-step approach: the backend
exposes entitlements data, and the frontend gates access.

### On login

1. User authenticates via OIDC — login always succeeds
2. `post_get_or_create_user` calls `get_user_entitlements()` with
   `force_refresh=True` to warm the cache
3. If entitlements are unavailable, a warning is logged but login
   proceeds

### On page load

1. Frontend calls `GET /users/me/` which includes `can_access`
2. If `can_access` is `false`, the frontend redirects to `/no-access`
3. The user remains authenticated — they see the header, logo, and
   their profile, but cannot use the app
4. The `/no-access` page offers a logout button and a message to
   contact support

This approach ensures users always have a session (important for
shared calendars and other interactions) while still gating access to
the main application.

### Caching behavior

- The DeployCenter backend caches results in Django's cache framework
  (key: `entitlements:user:{user_sub}`, TTL:
  `ENTITLEMENTS_CACHE_TIMEOUT`).
- On login, `force_refresh=True` bypasses the cache for fresh data.
- If the API fails during a forced refresh, stale cached data is
  returned as fallback.
- Subsequent `GET /users/me/` calls use the cached value (no
  `force_refresh`).

## Frontend

Users denied access see `/no-access` — a page using the main layout
(header with logo and user profile visible) with:

- A message explaining the app is not available for their account
- A suggestion to contact support
- A logout button

The user is fully authenticated and can see their profile in the
header, but cannot access calendars or events.

## Key files

| Area | Path |
|------|------|
| Service layer | `src/backend/core/entitlements/__init__.py` |
| Backend factory | `src/backend/core/entitlements/factory.py` |
| Abstract base | `src/backend/core/entitlements/backends/base.py` |
| Local backend | `src/backend/core/entitlements/backends/local.py` |
| DeployCenter backend | `src/backend/core/entitlements/backends/deploycenter.py` |
| Auth integration | `src/backend/core/authentication/backends.py` |
| User API serializer | `src/backend/core/api/serializers.py` |
| Calendar gating (signal) | `src/backend/core/signals.py` |
| CalDAV proxy gating | `src/backend/core/api/viewsets_caldav.py` |
| Import events gating | `src/backend/core/api/viewsets.py` |
| No-access page | `src/frontend/apps/calendars/src/pages/no-access.tsx` |
| Homepage gate | `src/frontend/apps/calendars/src/pages/index.tsx` |
| Calendar gate | `src/frontend/apps/calendars/src/pages/calendar.tsx` |
| Tests | `src/backend/core/tests/test_entitlements.py` |
