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 anazp(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
-
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.
-
sync_roles — Reconcile the user's local role assignments from the RAW realm roles on the token. The store decides which of
role_namesare 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
wittgenstein_oidc_backend.provisioning.UserStore.link_keycloak_identity
method UserStore.link_keycloak_identity(user: Any, *, keycloak_sub: str, keycloak_client_id: str | None) → 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