504 lines
17 KiB
Markdown
504 lines
17 KiB
Markdown
|
|
# Organizations
|
||
|
|
|
||
|
|
How organizations and multi-tenancy work in La Suite Calendars:
|
||
|
|
scoping users, calendars, resources, and permissions by
|
||
|
|
organization.
|
||
|
|
|
||
|
|
## Table of Contents
|
||
|
|
|
||
|
|
- [Overview](#overview)
|
||
|
|
- [What Is an Organization in Calendars?](#what-is-an-organization-in-calendars)
|
||
|
|
- [Where Organization Context Comes From](#where-organization-context-comes-from)
|
||
|
|
- [OIDC Claims](#oidc-claims)
|
||
|
|
- [DeployCenter and Entitlements](#deploycenter-and-entitlements)
|
||
|
|
- [What Is Org-Scoped](#what-is-org-scoped)
|
||
|
|
- [Data Model](#data-model)
|
||
|
|
- [Auto-Population on Login](#auto-population-on-login)
|
||
|
|
- [Why a Local Model?](#why-a-local-model)
|
||
|
|
- [CalDAV and Multi-Tenancy](#caldav-and-multi-tenancy)
|
||
|
|
- [The Core Problem](#the-core-problem)
|
||
|
|
- [SabreDAV Principal Backend Filtering](#sabredav-principal-backend-filtering)
|
||
|
|
- [User Discovery and Sharing](#user-discovery-and-sharing)
|
||
|
|
- [Resource Scoping](#resource-scoping)
|
||
|
|
- [Entitlements Integration](#entitlements-integration)
|
||
|
|
- [Frontend Considerations](#frontend-considerations)
|
||
|
|
- [Implementation Plan](#implementation-plan)
|
||
|
|
- [Phase 1: Org Context Propagation](#phase-1-org-context-propagation)
|
||
|
|
- [Phase 2: User Discovery Scoping](#phase-2-user-discovery-scoping)
|
||
|
|
- [Phase 3: CalDAV Scoping](#phase-3-caldav-scoping)
|
||
|
|
- [Phase 4: Resource Scoping](#phase-4-resource-scoping)
|
||
|
|
- [Key Files](#key-files)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
La Suite Calendars scopes users, calendars, and resources by
|
||
|
|
organization. Every user belongs to exactly one org, determined by
|
||
|
|
their email domain (default) or a configurable OIDC claim. Orgs
|
||
|
|
are created automatically on first login.
|
||
|
|
|
||
|
|
The overarching constraint is that **CalDAV has no native concept of
|
||
|
|
organizations or tenants**. The protocol operates on principals,
|
||
|
|
calendars, and scheduling. Org scoping is layered on top via
|
||
|
|
SabreDAV backend filtering.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## What Is an Organization in Calendars?
|
||
|
|
|
||
|
|
An organization is a **boundary for user and resource visibility**.
|
||
|
|
Within an organization:
|
||
|
|
|
||
|
|
- Users discover and share calendars with other members
|
||
|
|
- Resources (meeting rooms, equipment) are visible and bookable
|
||
|
|
- Admins manage resources and org-level settings
|
||
|
|
|
||
|
|
Across organizations:
|
||
|
|
|
||
|
|
- Users cannot discover each other (unless explicitly shared with
|
||
|
|
by email)
|
||
|
|
- Resources are invisible
|
||
|
|
- Scheduling still works via email (iTIP), just like scheduling
|
||
|
|
with external users
|
||
|
|
|
||
|
|
An organization maps to a real-world entity: a company, a government
|
||
|
|
agency, a university department. It is identified by a **unique ID**
|
||
|
|
-- either a specific OIDC claim value (like a SIRET number in
|
||
|
|
France) or an **email domain** (the default).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Where Organization Context Comes From
|
||
|
|
|
||
|
|
### OIDC Claims
|
||
|
|
|
||
|
|
The user's organization is identified at authentication time via an
|
||
|
|
OIDC claim. The identity provider (Keycloak) includes an org
|
||
|
|
identifier in the user info response:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"sub": "abc-123",
|
||
|
|
"email": "alice@ministry.gouv.fr",
|
||
|
|
"siret": "13002526500013"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The claim used to identify the org (e.g. `siret`) is configured via
|
||
|
|
`OIDC_USERINFO_ORGANIZATION_CLAIM`. When no claim is configured, the email
|
||
|
|
domain is used as the org identifier.
|
||
|
|
|
||
|
|
The claim names and their meaning depend on the Keycloak
|
||
|
|
configuration and the identity federation in use (AgentConnect for
|
||
|
|
French public sector, ProConnect, etc.).
|
||
|
|
|
||
|
|
### DeployCenter and Entitlements
|
||
|
|
|
||
|
|
The [entitlements system](entitlements.md) already forwards
|
||
|
|
OIDC claims to DeployCenter. The `oidc_claims` parameter in the
|
||
|
|
DeployCenter backend config specifies which claims to include:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"oidc_claims": ["siret"]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
DeployCenter uses the `siret` claim to determine which organization
|
||
|
|
the user belongs to and whether they have access to the Calendars
|
||
|
|
service. It also knows the **organization name** (e.g. "Ministere
|
||
|
|
X") and returns it in the entitlements response. This means the
|
||
|
|
entitlements system is the **source of truth for org names** --
|
||
|
|
Calendars does not need a separate OIDC claim for the org name.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## What Is Org-Scoped
|
||
|
|
|
||
|
|
| Feature | Behavior |
|
||
|
|
|---------|----------|
|
||
|
|
| **User discovery** (search when sharing) | Same-org users only |
|
||
|
|
| **Calendar sharing suggestions** | Same-org users; cross-org by typing full email |
|
||
|
|
| **Resource discovery** | Same-org resources only |
|
||
|
|
| **Resource creation** | Org admins only (`can_admin` entitlement) |
|
||
|
|
| **Resource booking** | Same-org users only |
|
||
|
|
| **Free/busy lookup** | Same-org principals |
|
||
|
|
|
||
|
|
Things that are **not** org-scoped:
|
||
|
|
|
||
|
|
- **Event scheduling via email**: iTIP works across orgs (same as
|
||
|
|
external users)
|
||
|
|
- **Calendar sharing by email**: A user can share a calendar with
|
||
|
|
anyone by typing their email address
|
||
|
|
- **CalDAV protocol operations**: PUT, GET, PROPFIND on a user's
|
||
|
|
own calendars
|
||
|
|
- **Subscription tokens**: Public iCal URLs
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Data Model
|
||
|
|
|
||
|
|
A lightweight `Organization` model stores just enough to scope
|
||
|
|
data. It is auto-populated on login from the OIDC claim (or email
|
||
|
|
domain) and the entitlements response.
|
||
|
|
|
||
|
|
```python
|
||
|
|
class Organization(BaseModel):
|
||
|
|
"""Organization model, populated from OIDC claims and entitlements."""
|
||
|
|
name = models.CharField(max_length=200, blank=True)
|
||
|
|
external_id = models.CharField(
|
||
|
|
max_length=128, unique=True, db_index=True
|
||
|
|
)
|
||
|
|
|
||
|
|
class Meta:
|
||
|
|
db_table = "calendars_organization"
|
||
|
|
```
|
||
|
|
|
||
|
|
A FK on User links each user to their org:
|
||
|
|
|
||
|
|
```python
|
||
|
|
class User(AbstractBaseUser, ...):
|
||
|
|
organization = models.ForeignKey(
|
||
|
|
Organization, on_delete=models.PROTECT, related_name="members"
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Auto-Population on Login
|
||
|
|
|
||
|
|
On OIDC login, `post_get_or_create_user()` resolves the org. The
|
||
|
|
org identifier (`external_id`) comes from the OIDC claim or
|
||
|
|
email domain. The org **name** comes from the entitlements response.
|
||
|
|
|
||
|
|
```python
|
||
|
|
# 1. Determine the org identifier
|
||
|
|
claim_key = settings.OIDC_USERINFO_ORGANIZATION_CLAIM # e.g. "siret"
|
||
|
|
if claim_key:
|
||
|
|
reg_id = user_info.get(claim_key)
|
||
|
|
else:
|
||
|
|
# Default: derive org from email domain
|
||
|
|
reg_id = user.email.split("@")[-1] if user.email and "@" in user.email else None
|
||
|
|
|
||
|
|
# 2. Get org name from entitlements (looked up from DeployCenter)
|
||
|
|
org_name = entitlements.get("organization_name", "")
|
||
|
|
|
||
|
|
# 3. Create or update the org
|
||
|
|
if reg_id:
|
||
|
|
org, created = Organization.objects.get_or_create(
|
||
|
|
external_id=reg_id,
|
||
|
|
defaults={"name": org_name}
|
||
|
|
)
|
||
|
|
if not created and org_name and org.name != org_name:
|
||
|
|
org.name = org_name
|
||
|
|
org.save(update_fields=["name"])
|
||
|
|
if user.organization_id != org.id:
|
||
|
|
user.organization = org
|
||
|
|
user.save(update_fields=["organization"])
|
||
|
|
```
|
||
|
|
|
||
|
|
By default, the org is derived from the **user's email domain**
|
||
|
|
(e.g. `alice@ministry.gouv.fr` → org `ministry.gouv.fr`). Orgs
|
||
|
|
are always created automatically on first login.
|
||
|
|
|
||
|
|
`OIDC_USERINFO_ORGANIZATION_CLAIM` can override this with a specific OIDC
|
||
|
|
claim (e.g. `"siret"` for French public sector, `"organization_id"`
|
||
|
|
for other identity providers).
|
||
|
|
|
||
|
|
The org name is kept in sync: each login updates it from the
|
||
|
|
entitlements response if it has changed. If entitlements are
|
||
|
|
unavailable on login (fail-open), the org is still created from
|
||
|
|
the OIDC claim or email domain, but the name is left empty until
|
||
|
|
a subsequent login succeeds.
|
||
|
|
|
||
|
|
### Why a Local Model?
|
||
|
|
|
||
|
|
- **Efficient queries**: `User.objects.filter(organization=org)`
|
||
|
|
for user search scoping, instead of JSONField queries on claims
|
||
|
|
- **Org-level settings**: Place to attach resource creation policy,
|
||
|
|
default timezone, branding, etc.
|
||
|
|
- **SabreDAV integration**: The org's Django UUID is forwarded to
|
||
|
|
SabreDAV as `X-CalDAV-Organization` for principal scoping
|
||
|
|
- **Claim-agnostic**: The claim name is a setting, not hardcoded
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## CalDAV and Multi-Tenancy
|
||
|
|
|
||
|
|
### The Core Problem
|
||
|
|
|
||
|
|
CalDAV principals live in a flat namespace: `principals/{username}`.
|
||
|
|
When a frontend does a `PROPFIND` on `principals/` or a
|
||
|
|
`principal-property-search`, SabreDAV returns **all** principals.
|
||
|
|
There is no built-in way to scope results by organization.
|
||
|
|
|
||
|
|
The same applies to scheduling: `calendar-free-busy-set` returns
|
||
|
|
free/busy for any principal the server knows about.
|
||
|
|
|
||
|
|
A design principle is that **Django should not inspect or filter
|
||
|
|
CalDAV traffic**. The `CalDAVProxyView` is a pass-through proxy --
|
||
|
|
it sets authentication headers and forwards requests, but never
|
||
|
|
parses CalDAV XML. Org scoping must happen either in SabreDAV
|
||
|
|
itself or in the frontend.
|
||
|
|
|
||
|
|
### SabreDAV Principal Backend Filtering
|
||
|
|
|
||
|
|
Org scoping is enforced server-side in SabreDAV by filtering
|
||
|
|
principal queries by `org_id`. Django never inspects CalDAV
|
||
|
|
traffic -- it only sets the `X-CalDAV-Organization` header.
|
||
|
|
|
||
|
|
```php
|
||
|
|
class OrgAwarePrincipalBackend extends AutoCreatePrincipalBackend
|
||
|
|
{
|
||
|
|
public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof')
|
||
|
|
{
|
||
|
|
$orgId = $this->server->httpRequest->getHeader('X-CalDAV-Organization');
|
||
|
|
// Add WHERE org_id = $orgId to the query
|
||
|
|
return parent::searchPrincipals(...) + org filter;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Implementation:**
|
||
|
|
1. Add `org_id` column to `principals` table
|
||
|
|
2. Set it when auto-creating principals (from `X-CalDAV-Organization`)
|
||
|
|
3. Filter discovery and listing methods by org (see below)
|
||
|
|
4. `CalDAVProxyView` always sets `X-CalDAV-Organization` from the
|
||
|
|
authenticated user's org
|
||
|
|
|
||
|
|
**Which backend methods are org-filtered:**
|
||
|
|
|
||
|
|
| Method | Filtered? | Why |
|
||
|
|
|--------|-----------|-----|
|
||
|
|
| `searchPrincipals()` | Yes | Used for user/resource discovery |
|
||
|
|
| `getPrincipalsByPrefix()` | Yes | Used for listing principals |
|
||
|
|
| `getPrincipalByPath()` | **No** | Used for sharing and scheduling with a specific principal — must work cross-org |
|
||
|
|
| Schedule outbox free/busy | Yes | Aggregates all calendars for a principal — scoped to same-org |
|
||
|
|
| `free-busy-query` on a specific calendar | **No** | If a user has access to a shared calendar, they can query its free/busy regardless of org |
|
||
|
|
|
||
|
|
This keeps principal paths stable (`principals/{username}` -- no
|
||
|
|
org baked into the URI), enforces scoping at the CalDAV level for
|
||
|
|
both web and external clients (Apple Calendar, Thunderbird), and
|
||
|
|
allows cross-org sharing to work when explicitly granted.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## User Discovery and Sharing
|
||
|
|
|
||
|
|
When a user types an email to share a calendar, the frontend
|
||
|
|
currently searches all users. With orgs:
|
||
|
|
|
||
|
|
### Same-Org Discovery
|
||
|
|
|
||
|
|
The user search endpoint (`GET /api/v1.0/users/?q=alice`) should
|
||
|
|
return only users in the same organization by default. This is a
|
||
|
|
Django-side filter:
|
||
|
|
|
||
|
|
```python
|
||
|
|
# In UserViewSet.get_queryset():
|
||
|
|
queryset = queryset.filter(organization=request.user.organization)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Cross-Org Sharing
|
||
|
|
|
||
|
|
Typing a full email address that doesn't match any same-org user
|
||
|
|
should still work. The frontend sends the sharing request to CalDAV
|
||
|
|
with the email address. SabreDAV resolves the recipient via
|
||
|
|
`getPrincipalByPath()`, which is **not org-filtered** -- so
|
||
|
|
cross-org sharing works. If the recipient is external (not on this
|
||
|
|
server), an iTIP email is sent.
|
||
|
|
|
||
|
|
Once shared, the recipient can see that calendar's events and
|
||
|
|
query its free/busy (via `free-busy-query` on the specific
|
||
|
|
calendar collection), regardless of org. They still cannot
|
||
|
|
discover the sharer's other calendars or query their aggregate
|
||
|
|
free/busy via the scheduling outbox.
|
||
|
|
|
||
|
|
The UI should make this distinction clear:
|
||
|
|
- Autocomplete results: same-org users
|
||
|
|
- Manual email entry: "This user is outside your organization"
|
||
|
|
|
||
|
|
### CalDAV User Search
|
||
|
|
|
||
|
|
The CalDAV `principal-property-search` REPORT is how external
|
||
|
|
CalDAV clients discover users. SabreDAV only returns principals
|
||
|
|
from the user's org.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Resource Scoping
|
||
|
|
|
||
|
|
Resource discovery and booking scoping follows the same pattern as
|
||
|
|
user scoping. See [docs/resources.md](resources.md) for the full
|
||
|
|
resource design.
|
||
|
|
|
||
|
|
Key points for org scoping of resources:
|
||
|
|
|
||
|
|
1. **Resource principals** get an org association (same `org_id`
|
||
|
|
column on the `principals` table as user principals)
|
||
|
|
2. **Resource discovery** is scoped to the user's org
|
||
|
|
3. **Resource creation** requires an org-admin permission
|
||
|
|
4. **Resource email addresses** follow the convention
|
||
|
|
`{opaque-id}@resource.calendar.{APP_DOMAIN}` -- the org is
|
||
|
|
**not** encoded in the email address (see resources.md for
|
||
|
|
rationale)
|
||
|
|
5. **No cross-org resource booking** -- the auto-schedule plugin
|
||
|
|
rejects invitations from users outside the resource's org
|
||
|
|
|
||
|
|
The resource creation permission gate checks the user's
|
||
|
|
`can_admin` entitlement, returned by the entitlements system
|
||
|
|
alongside `can_access`.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Entitlements Integration
|
||
|
|
|
||
|
|
The [entitlements system](entitlements.md) controls whether a user
|
||
|
|
can access Calendars at all. Organizations add a layer on top:
|
||
|
|
|
||
|
|
```
|
||
|
|
User authenticates
|
||
|
|
→ Entitlements check: can_access? (DeployCenter, per-user)
|
||
|
|
→ Org resolution: which org? (OIDC claim or email domain)
|
||
|
|
→ Org name: from entitlements response
|
||
|
|
→ Scoping: show only org's users/resources
|
||
|
|
```
|
||
|
|
|
||
|
|
The entitlements backend already receives OIDC claims (including
|
||
|
|
`siret`). DeployCenter resolves the organization and returns the
|
||
|
|
org name alongside the access decision:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"can_access": true,
|
||
|
|
"can_admin": false,
|
||
|
|
"organization_name": "Ministere X"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
On login, `post_get_or_create_user()` uses the entitlements
|
||
|
|
response to populate the organization name. The org's
|
||
|
|
`external_id` is determined locally (from the OIDC claim or
|
||
|
|
email domain), but the **display name comes from DeployCenter**.
|
||
|
|
This avoids requiring a separate OIDC claim for the org name and
|
||
|
|
keeps DeployCenter as the single source of truth for org metadata.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Frontend Considerations
|
||
|
|
|
||
|
|
### Org-Aware UI Elements
|
||
|
|
|
||
|
|
- **User search/autocomplete:** Filter results to same-org users by
|
||
|
|
default; show a "search all users" or "invite by email" option for
|
||
|
|
cross-org
|
||
|
|
- **Resource picker:** Only show resources from the user's org
|
||
|
|
- **Calendar list:** No change (users only see calendars they own or
|
||
|
|
are shared on)
|
||
|
|
- **No-access page:** Already exists (from entitlements). Could show
|
||
|
|
org-specific messaging
|
||
|
|
- **Org switcher:** Not needed (a user belongs to exactly one org)
|
||
|
|
|
||
|
|
### Org Context in Frontend State
|
||
|
|
|
||
|
|
The frontend needs to know the user's org ID to:
|
||
|
|
- Scope user search API calls
|
||
|
|
- Scope resource PROPFIND requests
|
||
|
|
- Display org name in the UI
|
||
|
|
|
||
|
|
This can come from the `GET /users/me/` response:
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"id": "user-uuid",
|
||
|
|
"email": "alice@ministry.gouv.fr",
|
||
|
|
"organization": {
|
||
|
|
"id": "org-uuid",
|
||
|
|
"name": "Ministere X"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Plan
|
||
|
|
|
||
|
|
### Phase 1: Org Context Propagation
|
||
|
|
|
||
|
|
**Goal:** Every request knows the user's org.
|
||
|
|
|
||
|
|
1. Add `OIDC_USERINFO_ORGANIZATION_CLAIM` setting (default: `""`, uses email
|
||
|
|
domain)
|
||
|
|
2. Add `Organization` model (id, name, external_id)
|
||
|
|
3. Add `organization` FK on `User` (non-nullable -- every user has
|
||
|
|
an org)
|
||
|
|
4. In `post_get_or_create_user()`, resolve org from email domain or
|
||
|
|
OIDC claim and set `user.organization`
|
||
|
|
5. Expose `organization` in `GET /users/me/` response
|
||
|
|
6. Frontend stores org context from `/users/me/`
|
||
|
|
|
||
|
|
### Phase 2: User Discovery Scoping
|
||
|
|
|
||
|
|
**Goal:** User search returns same-org users by default.
|
||
|
|
|
||
|
|
1. Scope `UserViewSet` queryset by org when org is set
|
||
|
|
2. Frontend user search autocomplete uses scoped endpoint
|
||
|
|
3. Cross-org sharing still works via explicit email entry
|
||
|
|
4. Add `X-CalDAV-Organization` header to `CalDAVProxyView` requests
|
||
|
|
|
||
|
|
### Phase 3: CalDAV Scoping
|
||
|
|
|
||
|
|
**Goal:** CalDAV operations are org-scoped.
|
||
|
|
|
||
|
|
1. Add `org_id` column to SabreDAV `principals` table
|
||
|
|
2. Set `org_id` when auto-creating principals (from
|
||
|
|
`X-CalDAV-Organization`)
|
||
|
|
3. Extend `AutoCreatePrincipalBackend` to filter
|
||
|
|
`searchPrincipals()`, `getPrincipalsByPrefix()`, and free/busy
|
||
|
|
by org
|
||
|
|
4. Test with external CalDAV clients (Apple Calendar, Thunderbird)
|
||
|
|
|
||
|
|
### Phase 4: Resource Scoping
|
||
|
|
|
||
|
|
**Goal:** Resources are org-scoped (depends on resources being
|
||
|
|
implemented -- see [docs/resources.md](resources.md)).
|
||
|
|
|
||
|
|
1. Resource creation endpoint requires org-admin permission
|
||
|
|
2. Resource principals get org association
|
||
|
|
3. Resource discovery is scoped by org
|
||
|
|
4. Resource booking respects org boundaries
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Files
|
||
|
|
|
||
|
|
| Area | Path |
|
||
|
|
|------|------|
|
||
|
|
| User model (claims) | `src/backend/core/models.py` |
|
||
|
|
| OIDC auth backend | `src/backend/core/authentication/backends.py` |
|
||
|
|
| OIDC settings | `src/backend/calendars/settings.py` |
|
||
|
|
| CalDAV proxy | `src/backend/core/api/viewsets_caldav.py` |
|
||
|
|
| Entitlements system | `src/backend/core/entitlements/` |
|
||
|
|
| User serializer | `src/backend/core/api/serializers.py` |
|
||
|
|
| SabreDAV principal backend | `src/caldav/src/AutoCreatePrincipalBackend.php` |
|
||
|
|
| SabreDAV server config | `src/caldav/server.php` |
|
||
|
|
| Resource scoping details | `docs/resources.md` |
|
||
|
|
| Entitlements details | `docs/entitlements.md` |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Design Decisions
|
||
|
|
|
||
|
|
1. **A user belongs to exactly one org**, determined by the OIDC
|
||
|
|
claim at login.
|
||
|
|
2. **Cross-org calendar sharing is allowed** -- a user can share by
|
||
|
|
email with anyone. Autocomplete only shows same-org users;
|
||
|
|
cross-org sharing requires typing the full email.
|
||
|
|
3. **Cross-org resource booking is not allowed** -- the
|
||
|
|
auto-schedule plugin rejects invitations from users outside the
|
||
|
|
resource's org.
|
||
|
|
4. **Org scoping is enforced in SabreDAV**, not in Django. Django
|
||
|
|
only sets `X-CalDAV-Organization` on proxied requests.
|
||
|
|
5. **Org is derived from email domain by default**. A specific OIDC
|
||
|
|
claim can be configured via `OIDC_USERINFO_ORGANIZATION_CLAIM` (e.g.
|
||
|
|
`siret` for French public sector).
|