feat: reserve administrative roles for staff email addresses

This commit is contained in:
2026-09-17 18:13:52 +02:00
parent 725e32daab
commit 540cd8eeb7
11 changed files with 230 additions and 70 deletions
+1 -1
View File
@@ -381,7 +381,7 @@ OIDC-style claims for the authenticated user. The response contains only the cla
- always: `email`
- scope `id`: `person_id`, `address_id`
- scope `roles`: `roles` (array; only the roles that actually grant something are exported — the implicit baseline role is stripped, and so are the `*_PENDING` markers of roles the BusPro CRM claims but nobody has approved yet, see `docs/user-roles.md`)
- scope `roles`: `roles` (array; only the roles that actually grant something are exported — the implicit baseline role is stripped, and so are the `*_PENDING` markers of roles the BusPro CRM claims but nobody has approved yet, see `docs/buspronet-schema/crm-selection-queries.md#from-claim-to-role`)
- scope `profile`: `profile` object:
```json
+4
View File
@@ -577,6 +577,10 @@ Known role mapping from the XML parser:
- admin users also receive default hotel code `SSL`
- no matched role means `ROLE_CUSTOMER`
These are the CRM's claims. The portal filters them before storing anything (staff-only
roles, approval markers), so a proxy must return the claims unfiltered — see
`docs/buspronet-schema/crm-selection-queries.md#from-claim-to-role`.
Candidate SQL for selected customer selections:
```sql
+34 -5
View File
@@ -29,6 +29,35 @@ Symfony roles:
Only selections with `auswahl="True"` count. Every `Admin/*` controller is gated on
one of these roles.
These are *claims*, not grants. What an account ends up holding is decided by
`Role::sync()` (`src/Security/Role.php`) on every login — see
[From claim to role](#from-claim-to-role).
---
## From claim to role
BusPro backend users can edit their own CRM selections, so the claims above are
filtered before anything is stored:
| Role | Claimed without a staff address | Claimed with a staff address |
| --- | --- | --- |
| `ROLE_TEAMER` | granted | granted |
| `ROLE_HOUSE_MANAGER` | nominated | nominated |
| `ROLE_ADMIN`, `ROLE_TEAM_ADMIN`, `ROLE_MANAGER`, `ROLE_GROUPS_ADMIN`, `ROLE_GROUPS_MANAGER`, `ROLE_CUSTOMER_EXPERT` (`Role::EMPLOYEE_ONLY`) | **ignored** | nominated |
- **Staff address**: the login email's domain is listed in `employee_email_domains`
(`config/services.yaml`, exact match, never a suffix). Such an account also gets
`ROLE_EMPLOYEE`, which is derived from the address and not from the CRM.
- **Nominated**: stored as the marker `ROLE_X_PENDING`, which grants nothing until an
administrator approves it in `/admin/user`. Nominations are announced to
`role_nomination_notification_emails`.
- **Ignored**: treated exactly like a selection that is not set. No marker is stored,
and a role approved earlier is **revoked** on the next login. A leftover marker from
before the rule cannot be approved and disappears on that login too.
- Anything the CRM stops claiming is revoked, approved or not.
- An account left without any effective role gets `ROLE_CUSTOMER`.
---
## The four tables
@@ -398,11 +427,11 @@ BusProNet host is the fix, and no schema change is needed.
## Portal side: a correct XML is not enough
`BpnAuthenticator::createOrUpdateLocalUser()` snapshots the roles onto the local `User`
entity at login (`setRoles($crmAttributes->roles)` /
`setHotelCodes($crmAttributes->hotelCodes)`). Roles are **not** re-read per request. So
after fixing BusPro the affected user must **log out and log in again** before
`ROLE_GROUPS_ADMIN` takes effect — an active session keeps the stale role set.
`BpnAuthenticator::syncFromCrm()` writes the roles onto the local `User` entity at login,
through `Role::sync()` (see [From claim to role](#from-claim-to-role)), and the hotel codes
verbatim. Roles are **not** re-read per request. So after fixing BusPro the affected user
must **log out and log in again**, and even then `ROLE_GROUPS_ADMIN` only takes effect once
the account has a staff address and an administrator has approved the nomination.
---