adorsys / adorsys/keycloak-config-cli

Allow marking roles as unalterable so imports never modify or delete them

Open
#1,673 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Java
Stars
1.2k
Forks
200
PR merge metrics
No merged PRs in 30d

Description

### Problem Statement

Keycloak 26.6.4+ blocks updating certain protected roles. There's no way to tell keycloak-config-cli to "leave these roles alone".

Two reasons we need it:

1. **Keycloak 26.6.4+ blocks them.** The realm roles `admin` and `create-realm`, and every role of the `realm-management` client, can no longer be updated or deleted via the Admin API (keycloak/keycloak#49624). Realm exports contain those roles. The moment one of them actually differs from the target instance — or `import.managed.role: full` tries to delete a role missing from the import — the import dies with a 403.

2. **Some roles shouldn't be config-managed.** Break-glass accounts, platform-superuser, audit roles. Today there's no way to say "config-cli owns everything except these".

### Proposed Solution

Add `import.unalterable-roles`. Matched roles are skipped instead of blowing up the import:

- **Update** → skipped, logged at `info` with the role name.
- **Delete** → never, even under `import.managed.role: full`.
- **Create** → allowed (nothing to protect yet).
- **Composites** of a matched role → also frozen.

Roles are identified the way a realm export identifies them — a `realm` list and a `client` map, `"*"` for all roles of a client:

```yaml
import:
unalterable-roles:
mode: add # add (default) | replace
realm:
- platform-superuser
client:
internal-audit: ["*"]
my-app: [tenant-admin]
```

Defaults, so the Keycloak case works with zero config:

```yaml
realm: [admin, create-realm]
client:
realm-management: ["*"]
```

`mode: add` merges with the defaults. `mode: replace` uses your list as-is — with empty lists it's a full opt-out back to today's behaviour.

### Alternatives considered

- **The current workaround: strip the offending roles from the export and set `import.managed.role: no-delete`.** Not workable. It means hand-editing every export in CI, and it forces you to give up role deletion entirely just to dodge a handful of roles you never wanted to manage anyway.
- **Fail with a clear error instead of skipping.** Doesn't help — the whole point is that an untouched realm export should import. Skipping also matches what `ClientImportService` already does for the `admin-permissions` client under FGAP V2.
- **A flat list of role names.** Can't tell a realm role from a client role with the same name, so it would over-protect the user's own roles.
- **Only enable this on Keycloak 26.6.4+.** Adds version branching and kills the governance use-case on older servers. `mode: replace` with empty lists is a cleaner opt-out.

### Use-case context

Promoting a realm export between environments (prod → a fresh environment) on Keycloak 26.6.4+. The export carries `admin`, `create-realm` and the `realm-management` roles; the target instance manages those itself. The import should apply everything else and leave them alone — without a post-processing step in the pipeline.

### Environment

- Keycloak Version: 26.7.3
- keycloak-config-cli Version: latest
- Java Version: 21+

### Additional information

Related but separate: role change detection currently compares the server-generated `id`/`containerId`, so roles copied from another instance look changed when they aren't (separate issue). Fixing that kills most of these 403s, but not all — it doesn't help when a protected role genuinely differs, and it doesn't help under `import.managed.role: full` where config-cli tries to delete them. Both are worth doing.

Known limitation to document: in the `master` realm, Keycloak creates a `-realm` client per realm, whose roles are blocked by the same upstream change. Those can't be matched by a literal client ID, so they're out of scope for now.

Also worth noting: protection applies to the role being *changed*, not the role being *referenced*. Your own role that lists `realm-management.manage-users` as a composite still syncs normally — Keycloak still allows mapping these roles.

### Acceptance Criteria

- [ ] As a user, I can declare unalterable realm and client roles in config, using the same realm/client shape as a realm export, with `"*"` as a client wildcard.
- [ ] Defaults ship out of the box: realm roles `admin` and `create-realm`, all `realm-management` client roles.
- [ ] As a user, I can choose whether my config extends the defaults (`add`, default) or replaces them (`replace`).
- [ ] `replace` with empty lists disables the feature entirely.
- [ ] An import that would update an unalterable role skips it, logs the name at `info`, and continues successfully.
- [ ] An unalterable role is never deleted, including under `import.managed.role: full`.
- [ ] Creating a role that matches the config is still allowed.
- [ ] Composites of an unalterable role aren't modified; composites of other roles that reference one still sync.
- [ ] A realm export containing the protected roles, with real differences, imports successfully on Keycloak 26.6.4+.
- [ ] Documented in the README, behaviour change recorded in the CHANGELOG.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.