Zum Inhalt

provisioning

module wittgenstein_oidc_backend.provisioning

Store-agnostic JIT (just-in-time) user provisioning from a Keycloak token — the same algorithm wittgenstein-core's own backend/src/auth.py implements inline (_jit_provision_user/_sync_roles_from_keycloak), extracted here parametrized over a UserStore so it doesn't depend on any particular ORM/schema. wittgenstein-core itself adopts this via a thin SqlAlchemyUserStore adapter over its own User/Role/UserRole models.

Classes

  • UserStore — The minimal interface a consuming app implements against its own user/role schema for jit_provision_user() to drive.

  • TokenMissingIdentityError — The token has neither an email (human) nor an azp (service-account client id) claim — there is nothing to match or create a user by.

Functions

  • jit_provision_user — Find-or-create the local user for this Keycloak identity, and sync its roles from the token's realm roles.

wittgenstein_oidc_backend.provisioning.UserStore

class UserStore()

Bases : Protocol

The minimal interface a consuming app implements against its own user/role schema for jit_provision_user() to drive.

Methods

  • find_by_email

  • find_by_client_id

  • link_keycloak_identity — Update an EXISTING user's Keycloak identity fields. Called on every match (not just first-time creation) so an account that existed before SSO was enabled gets linked to the Keycloak identity that first authenticates as it, instead of staying unlinked.

  • create_user

  • sync_roles — Reconcile the user's local role assignments from the RAW realm roles on the token. The store decides which of role_names are recognized (e.g. intersecting with its own Role table before assigning — wittgenstein-core's KEYCLOAK_KNOWN_ROLE_NAMES is one such filter) and reconciles to that set (add missing, remove extra). Called for both a freshly-provisioned user and an existing one on every token verification, so a role change in Keycloak takes effect without recreating the account.

wittgenstein_oidc_backend.provisioning.UserStore.find_by_email

method UserStore.find_by_email(email: str) → Any | None

wittgenstein_oidc_backend.provisioning.UserStore.find_by_client_id

method UserStore.find_by_client_id(client_id: str) → Any | None

Update an EXISTING user's Keycloak identity fields. Called on every match (not just first-time creation) so an account that existed before SSO was enabled gets linked to the Keycloak identity that first authenticates as it, instead of staying unlinked.

wittgenstein_oidc_backend.provisioning.UserStore.create_user

method UserStore.create_user(*, email: str | None, username: str, full_name: str | None, keycloak_sub: str, keycloak_client_id: str | None, is_service_account: bool) → Any

wittgenstein_oidc_backend.provisioning.UserStore.sync_roles

method UserStore.sync_roles(user: Any, role_names: set[str]) → None

Reconcile the user's local role assignments from the RAW realm roles on the token. The store decides which of role_names are recognized (e.g. intersecting with its own Role table before assigning — wittgenstein-core's KEYCLOAK_KNOWN_ROLE_NAMES is one such filter) and reconciles to that set (add missing, remove extra). Called for both a freshly-provisioned user and an existing one on every token verification, so a role change in Keycloak takes effect without recreating the account.

wittgenstein_oidc_backend.provisioning.TokenMissingIdentityError

class TokenMissingIdentityError()

Bases : Exception

The token has neither an email (human) nor an azp (service-account client id) claim — there is nothing to match or create a user by.

wittgenstein_oidc_backend.provisioning.jit_provision_user

jit_provision_user(payload: dict[str, Any], store: UserStore) → Any

Find-or-create the local user for this Keycloak identity, and sync its roles from the token's realm roles.

Human tokens are matched/created by email (so accounts that existed before SSO was enabled get linked instead of duplicated). Service-account tokens (Keycloak client-credentials grant — no email, only azp) are matched/created by keycloak_client_id; they carry no realm roles, so role sync is skipped for them (the store decides what a service account's default access looks like).

Raises