feat: additional roles per app

This commit is contained in:
Björn Fromme
2026-08-12 08:57:11 +02:00
parent f51d699928
commit b62abb1801
6 changed files with 152 additions and 33 deletions
+33 -10
View File
@@ -37,27 +37,42 @@ Register **one** confidential client for the BFF (Appsmith apps never get their
### Apps (`config/services.yaml` → `appsmith.apps`)
One entry per Appsmith app: its login-return URL and the Keycloak client role required to use it.
One entry per Appsmith app: its login-return URL, the Keycloak client role required to enter it, and the prefix marking that app's permission roles.
```yaml
appsmith.apps:
crm:
url: '%env(APP_CRM_LOGIN_URL)%'
role: 'app-crm-access'
role_prefix: 'app-crm-'
access_role: 'app-crm-access'
helpdesk:
url: '%env(APP_HELPDESK_LOGIN_URL)%'
role: 'app-helpdesk-access'
role_prefix: 'app-helpdesk-'
access_role: 'app-helpdesk-access'
```
Add the matching `APP_<KEY>_LOGIN_URL` in `.env.local`. No code changes needed.
Role setup in Keycloak:
1. On the BFF client → **Roles** → create one role per app (e.g. `app-crm-access`).
2. Create a group per app, assign the matching role under the group's **Role mapping**.
Keep prefixes non-overlapping — with `app-crm-` and `app-crm-admin-` as two apps, the first would swallow the second's roles.
### Roles and permissions
All roles live on the single BFF client. Two kinds, distinguished only by name:
- **Access role** (`access_role`) — the gate. Without it, login ends in `?error=access_denied`.
- **Permission roles** — every other role sharing the app's `role_prefix`. The prefix is stripped before the app sees them, so `app-crm-write` is reported as `write`. Adding a permission is a Keycloak role creation; nothing changes here or in the code.
Setup in Keycloak:
1. On the BFF client → **Roles** → create `app-crm-access` plus one role per permission (`app-crm-view`, `app-crm-write`, …).
2. Create a group per app (or per role bundle, e.g. `crm-editors`), assign roles under the group's **Role mapping**.
3. Put users in whichever groups they need.
4. Client → **Client scopes**`<client>-dedicated`**Mappers** → confirm a "client roles" mapper exists with **Add to access token** enabled (default for new clients).
Enforcement happens once, in `AuthController::callback`, by checking `resource_access.<client_id>.roles` on the access token right after login. The `/api/*` proxy does not re-check roles per request.
Where this is enforced: the access role is checked once, in `AuthController::callback`, against `resource_access.<client_id>.roles` on the access token right after login. The `/api/*` proxy does not re-check anything per request.
Permissions are **not** enforced by the BFF at all. `/auth/me` reports them so an app can hide or disable widgets, but anyone holding a `sid` can call the API directly. The backend is the authorization boundary and must check the roles claim on the access token the proxy injects.
Roles are re-read from Keycloak whenever the BFF refreshes the access token (roughly once per token lifetime), so granting or revoking a permission takes effect without a re-login. The access role is deliberately *not* re-checked there — revoking it takes effect at the next login, or immediately if you kill the Keycloak session.
### Backends (`config/services.yaml` → `backends`)
@@ -170,7 +185,7 @@ storeValue('sid', null);
navigateTo('LoginPage', {}, 'REPLACE');
```
**User profile** — one query `Bff_Me`: `GET /auth/me`, header `Authorization: Bearer {{appsmith.store.sid}}`. Returns:
**User profile and permissions** — one query `Bff_Me`: `GET /auth/me?app=crm`, header `Authorization: Bearer {{appsmith.store.sid}}`. The `app` key is required (hardcode the app's own key, same value it passes to `Bff.login`); one session is shared across every app the user has open, so the BFF needs to know who's asking to scope the permissions. Returns:
```json
{
@@ -179,11 +194,19 @@ navigateTo('LoginPage', {}, 'REPLACE');
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"preferred_username": "jdoe"
"preferred_username": "jdoe",
"app": "crm",
"permissions": ["access", "view", "write"]
}
```
Reference via `{{appsmith.store.user.email}}` etc. This is client-visible display data, not an authorization signal — the backend must still authorize off the real access token the BFF injects.
Reference via `{{appsmith.store.user.email}}` etc. Gate UI on permissions with
```js
{{ appsmith.store.user.permissions.includes('write') }}
```
This is client-visible display data, not an authorization signal — the backend must still authorize off the real access token the BFF injects.
## Known limitation