10 KiB
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.
Unlike La Suite Messages, Calendars only checks can_access — there
is no admin permission sync.
Architecture
┌─────────────────────────────────────────────┐
│ OIDC Authentication Backend │
│ post_get_or_create_user() — warms cache │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ UserMeSerializer │
│ GET /users/me/ → { can_access: bool } │
└──────────────┬──────────────────────────────┘
│
┌──────────────▼──────────────────────────────┐
│ 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): Publicget_user_entitlements()function andEntitlementsUnavailableErrorexception. - Backend factory (
core/entitlements/factory.py):@functools.cachesingleton that imports and instantiates the configured backend class. - Abstract base (
core/entitlements/backends/base.py): Defines theEntitlementsBackendinterface. - 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
- OIDC login (
core/authentication/backends.py):post_get_or_create_user()callsget_user_entitlements()withforce_refresh=Trueto warm the cache. Login always succeeds regardless ofcan_accessvalue — access is gated at API level and in the frontend. - User API (
core/api/serializers.py):UserMeSerializerexposescan_accessas a field onGET /users/me/. Fail-open: returnsTruewhen entitlements are unavailable. - Default calendar creation (
core/signals.py):provision_default_calendarchecks entitlements before creating a calendar for a new user. Fail-closed: skips creation when entitlements are unavailable. - CalDAV proxy (
core/api/viewsets_caldav.py): BlocksMKCALENDARandMKCOLmethods 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. - Import events (
core/api/viewsets.py): BlocksPOST /calendars/import-events/for non-entitled users. Fail-closed: denies import when entitlements are unavailable. - Frontend (
pages/index.tsx,pages/calendar.tsx): Checksuser.can_accessand redirects to/no-accesswhenfalse. 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_accessdefaults toTrue. - 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).
- The DeployCenter backend falls back to stale cached data when the API is unavailable.
EntitlementsUnavailableErroris 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:
{
"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
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:
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}
# 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}}
Access control flow
The entitlements check follows a two-step approach: the backend exposes entitlements data, and the frontend gates access.
On login
- User authenticates via OIDC — login always succeeds
post_get_or_create_usercallsget_user_entitlements()withforce_refresh=Trueto warm the cache- If entitlements are unavailable, a warning is logged but login proceeds
On page load
- Frontend calls
GET /users/me/which includescan_access - If
can_accessisfalse, the frontend redirects to/no-access - The user remains authenticated — they see the header, logo, and their profile, but cannot use the app
- The
/no-accesspage 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=Truebypasses 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 (noforce_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 |