From 41c93c9160eefa9488eaabf1c0b24b435520b6d3 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:11:07 +0200 Subject: [PATCH 01/18] build(flake): pin nix-tf-schema Generic Terraform-schema conversion helpers, so each pairing's resource surface can be derived from its provider's own schema rather than hand-written. Pinned source-only: nix-tf-schema's flake builds its `lib` from its own nixpkgs pin for x86_64 alone, and this flake evaluates for aarch64 too. We instantiate `conversion.nix` against our own `pkgs`, keeping one nixpkgs in play. This does make CI depend on git.fediversity.eu being reachable. --- flake.lock | 17 +++++++++++++++++ flake.nix | 12 ++++++++++++ 2 files changed, 29 insertions(+) diff --git a/flake.lock b/flake.lock index fe90317..0345f7a 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,21 @@ { "nodes": { + "nix-tf-schema": { + "flake": false, + "locked": { + "lastModified": 1785769754, + "narHash": "sha256-MsUM7F5R+44laHsgMXFddZacQ0NXFpN2SN6qHOmVtxc=", + "ref": "refs/heads/main", + "rev": "5d33a2ce2751e4b407e0269319457e088377077d", + "revCount": 8, + "type": "git", + "url": "https://git.fediversity.eu/fediversity/nix-tf-schema" + }, + "original": { + "type": "git", + "url": "https://git.fediversity.eu/fediversity/nix-tf-schema" + } + }, "nixpkgs": { "locked": { "lastModified": 1783522502, @@ -18,6 +34,7 @@ }, "root": { "inputs": { + "nix-tf-schema": "nix-tf-schema", "nixpkgs": "nixpkgs" } } diff --git a/flake.nix b/flake.nix index 9b61420..d438d22 100644 --- a/flake.nix +++ b/flake.nix @@ -3,6 +3,18 @@ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + # Generic Terraform-schema <-> Nix conversion helpers, used to derive each + # pairing's resource surface from its vendored provider schema. + # + # Pulled in source-only (`flake = false`): its own flake builds `lib` from + # *its* nixpkgs pin for x86_64 alone, and we evaluate for aarch64 too. We + # instantiate `conversion.nix` against our own `pkgs` instead, so there is one + # nixpkgs in play and both systems work. + inputs.nix-tf-schema = { + url = "git+https://git.fediversity.eu/fediversity/nix-tf-schema"; + flake = false; + }; + outputs = inputs: let From 2bd0017d91ed687faa791004c23cd7f58be3b750 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:18:42 +0200 Subject: [PATCH 02/18] feat(modules/lib): add schema-driven resourceTypes generator Hand-writing each pairing's resource surface makes provider drift invisible: an attribute added, removed or retyped upstream only surfaces when `tofu apply` fails, if at all. `mkResourceTypes` derives the surface from a vendored provider schema instead, so drift is an eval-time error at `nix flake check`. A pairing supplies a per-collection overlay carrying only what a schema cannot state -- the NixOS-facing descriptions, the reference graph, and a short list of documented corrections. There is deliberately no way to declare an option with no schema counterpart (`refs` is the sole exception), which is what makes the drift check total. `checks` is `deepSeq`'d into `resourceTypes`, so a pairing cannot use the generated surface without also firing every assertion. Nothing consumes this yet. Assisted-by: Claude:claude-opus-5 --- modules/lib/tf-schema.nix | 423 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 423 insertions(+) create mode 100644 modules/lib/tf-schema.nix diff --git a/modules/lib/tf-schema.nix b/modules/lib/tf-schema.nix new file mode 100644 index 0000000..d20d5ad --- /dev/null +++ b/modules/lib/tf-schema.nix @@ -0,0 +1,423 @@ +# derive a pairing's `resourceTypes` record from its vendored provider schema. +# +# why: a hand-written resource surface makes provider drift invisible. an +# attribute added, removed or retyped upstream only surfaces when `tofu apply` +# fails on someone's machine -- if at all. reflecting the schema instead turns +# every such change into an eval-time error at `nix flake check`, and leaves a +# pairing carrying only what a schema cannot express: the NixOS-facing +# descriptions, the reference graph between collections, and a short list of +# documented corrections (the "overlay"). +# +# split of responsibility: `nix-tf-schema` knows schema facts (which attributes +# are settable, both nesting dialects, TF type -> Nix type). everything here is +# about `resourceTypes` shape, secret indirection, and drift assertions. the +# renderer in ./default.nix consumes the result unchanged. +{ + pkgs, + nixTfSchema, +}: +let + inherit (pkgs) lib; + ty = lib.types; + conv = pkgs.callPackage "${nixTfSchema}/conversion.nix" { }; + + inherit (lib) + attrNames + concatMapAttrs + concatStringsSep + elem + filter + flatten + length + mapAttrsToList + optionalAttrs + subtractLists + unique + ; + + # schema `format_version`s this generator knows how to read. + knownFormatVersions = [ "1.0" ]; + + # terraform type constructors that make an attribute a collection. + collectionTypes = [ + "list" + "set" + "map" + ]; + + # `[ "list" "string" ]` -> `list(string)`, for generated descriptions. + renderTfType = + l: if length l <= 1 then lib.head l else "${lib.head l}(${renderTfType (lib.tail l)})"; + + sortStrings = lib.sort builtins.lessThan; + + # elements occurring more than once, deduplicated. + duplicates = xs: unique (filter (x: lib.count (y: y == x) xs > 1) xs); + + quoteList = xs: concatStringsSep ", " (map (x: "`${x}`") xs); +in +{ + /* + Build a pairing's `resourceTypes` (and its drift assertions) from a vendored + provider schema plus a per-collection overlay. + + schema parsed `provider-schema.json` (normalized: `format_version`, + `source`, `version`, `resource_schemas`) + provider the packaged provider derivation; its `version` must match + the vendored schema + source provider source address, e.g. "svalabs/forgejo" + runtimePrefix "services..runtime" -- for error messages + resources collection name -> overlay (see below) + unsupported schema resource type -> non-empty reason for not modelling it + + An overlay's first six fields are mandatory; the rest default to empty and + exist only to correct things a schema cannot state. There is deliberately no + way to declare an option with no schema counterpart -- `refs` is the sole + exception, and that is what makes the drift check total. + + type schema key; must exist and be claimed exactly once + prefix unique Terraform label prefix + nameAttr attribute defaulted from the collection key, or null + scope provider token scope(s) the resource needs, or null + refs parent links (shape unchanged; see ./default.nix) + description the collection's NixOS option description + + oneOfRefs ExactlyOneOf ref groups (a Go validator, absent from schemas) + omit dotted paths to drop entirely + extraSecrets paths to treat as secret although not marked `sensitive` + notSecrets `sensitive` paths not to treat as secret + forceOptional paths required upstream but nullable here + requiredAttrs extra non-empty checks + extraAttrs last-resort typed overrides; keys must name real attributes + + Returns `{ resourceTypes; checks; }`. `checks` is a list of null-or-throw, + `deepSeq`'d into `resourceTypes`, so merely forcing the latter fires every + assertion -- a pairing cannot use the surface without also checking it. + */ + mkResourceTypes = + { + schema, + provider, + source, + runtimePrefix, + resources, + unsupported ? { }, + }: + let + check = cond: msg: if cond then null else throw "${runtimePrefix}: ${msg}"; + + withDefaults = + o: + { + oneOfRefs = [ ]; + omit = [ ]; + extraSecrets = [ ]; + notSecrets = [ ]; + forceOptional = [ ]; + requiredAttrs = [ ]; + extraAttrs = { }; + } + // o; + + # ----------------------------------------------------------------------- + # one collection + # ----------------------------------------------------------------------- + + mkOne = + collection: rawOverlay: + let + o = withDefaults rawOverlay; + ctx = "${runtimePrefix}.${collection}"; + # the type check has to come first: everything below indexes the + # schema by it. + resourceSchema = + schema.resource_schemas.${o.type} or (throw "${runtimePrefix}: collection '${collection}' models resource `${o.type}`, which provider ${source} ${provider.version} does not have"); + + # top-level nodes, and every node by dotted path. + tree = conv.settableTree resourceSchema; + paths = conv.settablePaths resourceSchema; + + # attributes the reference inputs occupy: the user names a sibling + # collection key and generation fills these in, so they must not also + # be settable options. + refConsumed = mapAttrsToList (_: r: r.attr) o.refs; + droppedPaths = o.omit ++ refConsumed; + isDropped = path: lib.any (d: path == d || lib.hasPrefix "${d}." path) droppedPaths; + + isSecret = + path: node: + node.kind == "attr" && (node.sensitive || elem path o.extraSecrets) && !(elem path o.notSecrets); + + isCollectionNode = + node: + if node.kind == "attr" then + elem (lib.head (flatten node.tfType)) collectionTypes + else + !node.singleBlock && elem node.nesting collectionTypes; + + isNameAttr = path: o.nameAttr != null && path == o.nameAttr; + + # an option is emitted required (no default) only when nothing else + # can supply it later. the four escapes: + # nameAttr -- the attrset key fills it, post-injection + # (./default.nix re-imposes it via requiredAttrs) + # secrets -- the `File` sibling may satisfy it instead + # collections -- listOf/attrsOf default to []/{} and cannot say + # "unset"; an empty value would silently strip + # server-side state + # forceOptional -- explicit, documented exceptions + isRequired = + path: node: + node.required + && !(elem path o.forceOptional) + && !(isSecret path node) + && !(isNameAttr path) + && !(isCollectionNode node); + + tfTypeLabel = + node: + if node.kind == "attr" then + renderTfType (flatten node.tfType) + else if node.singleBlock then + "single-item ${node.nesting} block" + else + "${node.nesting} block"; + + describe = + path: node: + if node.description != "" then + node.description + else + "Provider attribute `${path}` (`${tfTypeLabel node}`); the provider schema carries no description for it."; + + fileOption = + attr: + lib.mkOption { + type = ty.nullOr ty.str; + default = null; + description = "Runtime path to a file holding `${attr}` (loaded via systemd LoadCredential=; never copied to the store). Mutually exclusive with a literal `${attr}`."; + }; + + mkNodeOption = + path: node: + let + nixType = + if node.kind == "attr" then + conv.fromTfTypes (flatten node.tfType) + else + let + sub = ty.submodule { options = mkOptions path node.children; }; + in + if node.singleBlock then + sub + else + { + single = sub; + group = sub; + list = ty.listOf sub; + set = ty.listOf sub; # nix has no unordered collection type + map = ty.attrsOf sub; + } + .${node.nesting}; + in + lib.mkOption ( + ( + if isRequired path node then + { type = nixType; } + else + { + type = ty.nullOr nixType; + default = null; + } + ) + // { + description = describe path node; + } + ); + + # nested secrets get their `File` sibling here; top-level ones + # get theirs from `resourceOptions` in ./default.nix, off `secrets`. + mkOptions = + prefix: nodes: + concatMapAttrs ( + name: node: + let + path = if prefix == "" then name else "${prefix}.${name}"; + in + if isDropped path then + { } + else + { + ${name} = mkNodeOption path node; + } + // optionalAttrs (prefix != "" && isSecret path node) { + "${name}File" = fileOption name; + } + ) nodes; + + topLevel = filter (p: !(isDropped p)) (attrNames tree); + + secrets = sortStrings (filter (p: isSecret p tree.${p}) topLevel); + requiredSecrets = filter (p: tree.${p}.required && !(elem p o.forceOptional)) secrets; + + requiredAttrs = unique ( + filter ( + p: + let + node = tree.${p}; + in + node.required + && !(elem p o.forceOptional) + && !(isSecret p node) + && (isNameAttr p || isCollectionNode node) + ) topLevel + ++ o.requiredAttrs + ); + + # MaxItems:1 blocks: the user writes one object, terraform reads a + # one-element list. only the sdk/v2 dialect produces these. + blockAttrs = sortStrings (filter (p: paths.${p}.singleBlock && !(isDropped p)) (attrNames paths)); + + sensitivePaths = filter (p: paths.${p}.sensitive) (attrNames paths); + allPaths = attrNames paths; + + namesPaths = + label: xs: universe: + let + unknown = subtractLists universe xs; + in + check (unknown == [ ]) + "${ctx}: ${label} names ${ + if length unknown == 1 then "an attribute" else "attributes" + } `${o.type}` does not have: ${quoteList unknown}"; + + checks = [ + (namesPaths "`omit`" o.omit allPaths) + (namesPaths "`extraSecrets`" o.extraSecrets allPaths) + (namesPaths "`notSecrets`" o.notSecrets allPaths) + (namesPaths "`forceOptional`" o.forceOptional allPaths) + (namesPaths "`requiredAttrs`" o.requiredAttrs (attrNames tree)) + (namesPaths "`extraAttrs`" (attrNames o.extraAttrs) (attrNames tree)) + (namesPaths "`refs.*.attr`" refConsumed (attrNames tree)) + (check (filter (p: elem p sensitivePaths) o.extraSecrets == [ ]) + "${ctx}: `extraSecrets` lists ${ + quoteList (filter (p: elem p sensitivePaths) o.extraSecrets) + }, which the schema already marks sensitive" + ) + (check (subtractLists sensitivePaths o.notSecrets == [ ]) + "${ctx}: `notSecrets` may only list sensitive attributes; ${quoteList (subtractLists sensitivePaths o.notSecrets)} ${ + if length (subtractLists sensitivePaths o.notSecrets) == 1 then "is" else "are" + } not sensitive" + ) + (check + ( + o.nameAttr == null + || ( + tree ? ${o.nameAttr} + && tree.${o.nameAttr}.kind == "attr" + && flatten tree.${o.nameAttr}.tfType == [ "string" ] + ) + ) + "${ctx}: `nameAttr` must name a settable top-level string attribute; `${toString o.nameAttr}` is not one" + ) + ] + ++ mapAttrsToList ( + refName: refSpec: + check (lib.all (t: resources ? ${t.collection}) refSpec.targets) + "${ctx}: ref `${refName}` targets ${ + quoteList (filter (c: !(resources ? ${c})) (map (t: t.collection) refSpec.targets)) + }, which ${runtimePrefix} does not define" + ) o.refs + ++ map ( + group: + check (lib.all (r: o.refs ? ${r}) group) + "${ctx}: `oneOfRefs` names ${ + quoteList (filter (r: !(o.refs ? ${r})) group) + }, which ${ctx} does not declare as refs" + ) o.oneOfRefs; + + spec = { + inherit (o) + type + prefix + nameAttr + scope + refs + description + oneOfRefs + ; + inherit + secrets + requiredSecrets + requiredAttrs + blockAttrs + ; + attrs = mkOptions "" tree // o.extraAttrs; + }; + in + { + inherit spec checks; + }; + + built = lib.mapAttrs mkOne resources; + + # ----------------------------------------------------------------------- + # provider-wide assertions + # ----------------------------------------------------------------------- + + allTypes = mapAttrsToList (_: o: o.type) resources; + allPrefixes = mapAttrsToList (_: o: o.prefix) resources; + schemaTypes = attrNames schema.resource_schemas; + unsupportedTypes = attrNames unsupported; + + unclaimed = subtractLists (allTypes ++ unsupportedTypes) schemaTypes; + + globalChecks = [ + # identity: the cheap guards that fire the instant nixpkgs moves the + # provider under us. `-schema-current` is the authoritative check + # for content changes within a single version. + (check ( + schema.source == source + ) "vendored schema is for provider `${schema.source}`, but this pairing is `${source}`") + (check (schema.version == provider.version) + "vendored schema is for ${source} ${schema.version}, but the packaged provider is ${provider.version}; run `nix run .#update-provider-schemas`" + ) + (check (elem schema.format_version knownFormatVersions) "unrecognized schema `format_version` `${schema.format_version}` (known: ${quoteList knownFormatVersions})") + + (check ( + duplicates allTypes == [ ] + ) "resource ${quoteList (duplicates allTypes)} claimed by more than one collection") + (check ( + duplicates allPrefixes == [ ] + ) "label prefix ${quoteList (duplicates allPrefixes)} used by more than one collection") + + # schema -> overlay: a provider bump names every new resource here. + (check (unclaimed == [ ]) + "provider ${source} ${provider.version} has ${toString (length unclaimed)} resource(s) that are neither modelled nor listed in `unsupported`: ${quoteList unclaimed}" + ) + (check (subtractLists schemaTypes unsupportedTypes == [ ]) + "`unsupported` names ${quoteList (subtractLists schemaTypes unsupportedTypes)}, which provider ${source} ${provider.version} does not have" + ) + (check (filter (t: elem t allTypes) unsupportedTypes == [ ]) + "${quoteList (filter (t: elem t allTypes) unsupportedTypes)} ${ + if length (filter (t: elem t allTypes) unsupportedTypes) == 1 then "is" else "are" + } both modelled and listed in `unsupported`" + ) + (check (filter (t: unsupported.${t} == "") unsupportedTypes == [ ]) + "`unsupported` entries need a non-empty reason; ${ + quoteList (filter (t: unsupported.${t} == "") unsupportedTypes) + } ${ + if length (filter (t: unsupported.${t} == "") unsupportedTypes) == 1 then "has" else "have" + } none" + ) + ]; + + checks = globalChecks ++ lib.concatLists (mapAttrsToList (_: r: r.checks) built); + in + { + # forcing the surface forces every assertion: a pairing cannot use the + # generated options without also having checked them for drift. + resourceTypes = builtins.deepSeq checks (lib.mapAttrs (_: r: r.spec) built); + inherit checks; + }; +} From ee31283e05b5502f51946bd535beda236b6623f5 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:20:11 +0200 Subject: [PATCH 03/18] feat(flake): expose provider-schema packages and an update app `nix build .#-provider-schema` extracts the pinned provider's schema in a sandbox (`tofu providers schema -json`, offline via `opentofu.withPlugins`) and normalizes it. `nix run .#update-provider-schemas` installs both over the vendored copies, so a nixpkgs bump becomes: refresh, then let `nix flake check` name every resource and attribute that moved. The schemas are vendored rather than read at eval time: the flake evaluates for aarch64-linux as well, and IFD would mean running a foreign-arch provider binary during evaluation. Each pairing's `lib.nix` now exports `provider` and `providerSource`, the single source of truth the schema tooling reads. Assisted-by: Claude:claude-opus-5 --- flake.nix | 55 ++++++++++++++++++++++++++++++++++++++- services/forgejo/lib.nix | 18 ++++++++++--- services/keycloak/lib.nix | 18 ++++++++++--- 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index d438d22..3bbfa14 100644 --- a/flake.nix +++ b/flake.nix @@ -34,6 +34,33 @@ cfg ]; }; + + # the pairings, by service name. each `lib.nix` exposes the packaged + # provider and its source address, which is all the schema tooling needs. + pairingLibs = pkgs: { + forgejo = import ./services/forgejo/lib.nix { inherit pkgs; }; + keycloak = import ./services/keycloak/lib.nix { inherit pkgs; }; + }; + + # `-provider-schema`: the normalized schema of the pinned provider, + # extracted in a sandbox (`tofu providers schema -json`). This is the + # source for the vendored `services//provider-schema.json`; it is + # never imported at eval time, because the flake evaluates for aarch64 too + # and IFD would mean running a foreign-arch provider binary. + providerSchemas = + pkgs: + let + conv = pkgs.callPackage "${inputs.nix-tf-schema}/conversion.nix" { }; + in + lib.mapAttrs' ( + svc: l: + lib.nameValuePair "${svc}-provider-schema" ( + conv.mkProviderSchemaFile { + inherit (l) provider; + source = l.providerSource; + } + ) + ) (pairingLibs pkgs); in { # NixOS module entrypoint: enables a Nixpkgs service and reconciles its @@ -47,9 +74,35 @@ ) examples; packages = lib.mapAttrs ( - system: _pkgs: lib.mapAttrs (_name: cfg: (exampleSystem system cfg).config.system.build.vm) examples + system: pkgs: + lib.mapAttrs (_name: cfg: (exampleSystem system cfg).config.system.build.vm) examples + // providerSchemas pkgs ) inputs.nixpkgs.legacyPackages; + # `nix run .#update-provider-schemas` after a nixpkgs bump moves a + # provider: refresh the vendored schemas, then `nix flake check` reports + # every resource and attribute that changed. + apps = lib.mapAttrs (_system: pkgs: { + update-provider-schemas = { + type = "app"; + program = lib.getExe ( + pkgs.writeShellApplication { + name = "update-provider-schemas"; + runtimeInputs = [ pkgs.git ]; + text = '' + root=$(git rev-parse --show-toplevel) + ${lib.concatLines ( + lib.mapAttrsToList (name: drv: '' + install -Dm0644 ${drv} "$root/services/${lib.removeSuffix "-provider-schema" name}/provider-schema.json" + echo "updated services/${lib.removeSuffix "-provider-schema" name}/provider-schema.json" + '') (providerSchemas pkgs) + )} + ''; + } + ); + }; + }) inputs.nixpkgs.legacyPackages; + checks = lib.mapAttrs ( system: pkgs: import ./services/forgejo/checks.nix { diff --git a/services/forgejo/lib.nix b/services/forgejo/lib.nix index b4681ea..e8a69a3 100644 --- a/services/forgejo/lib.nix +++ b/services/forgejo/lib.nix @@ -17,6 +17,8 @@ let provider = import ./pkg.nix { inherit pkgs; }; providerVersion = provider.version; + # provider source address; also keys the vendored provider schema. + providerSource = "svalabs/forgejo"; tokenVar = "forgejo_api_token"; executor = pkgs.opentofu.withPlugins (_: [ provider ]); @@ -421,9 +423,13 @@ let if scopes == [ ] then "write:organization" else lib.concatStringsSep "," scopes; forgejoTfConfig = genlib.mkTfConfig { - inherit resourceTypes providerVersion tokenVar; + inherit + resourceTypes + providerVersion + providerSource + tokenVar + ; providerName = "forgejo"; - providerSource = "svalabs/forgejo"; runtimePrefix = "services.forgejo.runtime"; providerBlock = cfg: { host = cfg.baseUrl; @@ -432,7 +438,13 @@ let }; in { - inherit resourceTypes requiredScopes forgejoTfConfig; + inherit + provider + providerSource + resourceTypes + requiredScopes + forgejoTfConfig + ; resourceOptions = genlib.resourceOptions resourceTypes; mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; }); } diff --git a/services/keycloak/lib.nix b/services/keycloak/lib.nix index 7f60a11..0240269 100644 --- a/services/keycloak/lib.nix +++ b/services/keycloak/lib.nix @@ -17,6 +17,8 @@ let provider = pkgs.terraform-providers.keycloak_keycloak; providerVersion = provider.version; + # provider source address; also keys the vendored provider schema. + providerSource = "keycloak/keycloak"; # tf-var names for the service-account oauth2 client the reconciler uses. tokenVar = "keycloak_client_secret"; @@ -3138,9 +3140,13 @@ let }; keycloakTfConfig = genlib.mkTfConfig { - inherit resourceTypes providerVersion tokenVar; + inherit + resourceTypes + providerVersion + providerSource + tokenVar + ; providerName = "keycloak"; - providerSource = "keycloak/keycloak"; runtimePrefix = "services.keycloak.runtime"; extraSensitiveVars = [ clientIdVar ]; providerBlock = cfg: { @@ -3152,7 +3158,13 @@ let }; in { - inherit resourceTypes keycloakTfConfig clientIdVar; + inherit + provider + providerSource + resourceTypes + keycloakTfConfig + clientIdVar + ; resourceOptions = genlib.resourceOptions resourceTypes; mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; }); } From acafb6b739d2de1f2c0403acb0b9fdacb7ac99c9 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:21:36 +0200 Subject: [PATCH 04/18] feat(services/forgejo): vendor the provider schema `services/forgejo/provider-schema.json` is the normalized output of `tofu providers schema -json` for the pinned svalabs/forgejo 1.5.0, and `schema.nix` parses it. Committing it keeps evaluation IFD-free, which matters because the flake evaluates for aarch64-linux too. The new `forgejo-schema-current` check rebuilds the schema and diffs it against the vendored copy, so a provider whose schema changes without a version bump still fails CI. Nothing derives options from it yet. Assisted-by: Claude:claude-opus-5 --- flake.nix | 35 + services/forgejo/provider-schema.json | 1614 +++++++++++++++++++++++++ services/forgejo/schema.nix | 6 + 3 files changed, 1655 insertions(+) create mode 100644 services/forgejo/provider-schema.json create mode 100644 services/forgejo/schema.nix diff --git a/flake.nix b/flake.nix index 3bbfa14..b9b7c4d 100644 --- a/flake.nix +++ b/flake.nix @@ -61,6 +61,40 @@ } ) ) (pairingLibs pkgs); + + # `-schema-current`: the authoritative drift check. The eval-time + # assertions in `modules/lib/tf-schema.nix` compare version strings; this + # one compares content, so a provider that changes a schema without + # changing its version still fails CI. + # + # A pairing opts in by vendoring the file; once its `lib.nix` derives the + # resource surface from `schema.nix` the file is load-bearing and cannot + # quietly disappear again. + schemaChecks = + pkgs: + lib.mapAttrs' + ( + name: fresh: + let + svc = lib.removeSuffix "-provider-schema" name; + in + lib.nameValuePair "${svc}-schema-current" ( + pkgs.runCommand "${svc}-schema-current" { nativeBuildInputs = [ pkgs.diffutils ]; } '' + if ! diff -u ${./services}/${svc}/provider-schema.json ${fresh}; then + echo >&2 + echo "services/${svc}/provider-schema.json is stale; run 'nix run .#update-provider-schemas'" >&2 + exit 1 + fi + touch "$out" + '' + ) + ) + ( + lib.filterAttrs ( + name: _: + lib.pathExists (./services + "/${lib.removeSuffix "-provider-schema" name}/provider-schema.json") + ) (providerSchemas pkgs) + ); in { # NixOS module entrypoint: enables a Nixpkgs service and reconciles its @@ -117,6 +151,7 @@ name: cfg: lib.nameValuePair "example-${name}" (exampleSystem system cfg).config.system.build.toplevel ) examples) + // schemaChecks pkgs // { formatting = inputs.self.formatter.${system}.check inputs.self; } diff --git a/services/forgejo/provider-schema.json b/services/forgejo/provider-schema.json new file mode 100644 index 0000000..1b8ef6f --- /dev/null +++ b/services/forgejo/provider-schema.json @@ -0,0 +1,1614 @@ +{ + "format_version": "1.0", + "resource_schemas": { + "forgejo_branch_protection": { + "block": { + "attributes": { + "approvals_whitelist_teams": { + "computed": true, + "description": "Whitelisted teams for reviewing. **Note**: This setting is only effective if `enable_approvals_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "approvals_whitelist_usernames": { + "computed": true, + "description": "Whitelisted users for reviewing. **Note**: This setting is only effective if `enable_approvals_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "block_on_official_review_requests": { + "computed": true, + "description": "Block merge on official review requests.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "block_on_outdated_branch": { + "computed": true, + "description": "Block merge if pull request is outdated.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "block_on_rejected_reviews": { + "computed": true, + "description": "Block merge on rejected reviews.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "branch_name": { + "description": "Name of the branch to protect. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "dismiss_stale_approvals": { + "computed": true, + "description": "Dismiss stale approvals.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_approvals_whitelist": { + "computed": true, + "description": "Restrict approvals to whitelisted users or teams.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_merge_whitelist": { + "computed": true, + "description": "Restrict merge to whitelisted users or teams.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_push": { + "computed": true, + "description": "Enable push to the branch.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_push_whitelist": { + "computed": true, + "description": "Restrict push to whitelisted users or teams. **Note**: This setting is only effective if `enable_push` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_status_check": { + "computed": true, + "description": "Enable status check.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "merge_whitelist_teams": { + "computed": true, + "description": "Whitelisted teams for merging. **Note**: This setting is only effective if `enable_merge_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "merge_whitelist_usernames": { + "computed": true, + "description": "Whitelisted users for merging. **Note**: This setting is only effective if `enable_merge_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "protected_file_patterns": { + "computed": true, + "description": "Protected file patterns (separated using semicolon ';').", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "push_whitelist_deploy_keys": { + "computed": true, + "description": "Whitelist deploy keys with write access to push. **Note**: This setting is only effective if `enable_push_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "push_whitelist_teams": { + "computed": true, + "description": "Whitelisted teams for pushing. **Note**: This setting is only effective if `enable_push_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "push_whitelist_usernames": { + "computed": true, + "description": "Whitelisted users for pushing. **Note**: This setting is only effective if `enable_push_whitelist` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + }, + "require_signed_commits": { + "computed": true, + "description": "Require signed commits.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "required_approvals": { + "computed": true, + "description": "Number of required approvals.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "status_check_contexts": { + "computed": true, + "description": "Status check patterns. **Note**: This setting is only effective if `enable_status_check` is `true`.", + "description_kind": "plain", + "optional": true, + "type": [ + "list", + "string" + ] + }, + "unprotected_file_patterns": { + "computed": true, + "description": "Unprotected file patterns (separated using semicolon ';').", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description": "Forgejo branch protection resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_collaborator": { + "block": { + "attributes": { + "permission": { + "description": "Repository permissions of the collaborator. Must be one of 'read', 'write', 'admin'.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + }, + "user": { + "description": "Username of the collaborator. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description": "Forgejo repository collaborator resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_deploy_key": { + "block": { + "attributes": { + "created_at": { + "computed": true, + "description": "Time at which the deploy key was created.", + "description_kind": "plain", + "type": "string" + }, + "fingerprint": { + "computed": true, + "description": "Fingerprint of the deploy key.", + "description_kind": "plain", + "type": "string" + }, + "key": { + "description": "Armored SSH key. Trailing newlines must be removed (e.g. using trimspace() function). Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "key_id": { + "computed": true, + "description": "Numeric identifier of the deploy key.", + "description_kind": "plain", + "type": "number" + }, + "read_only": { + "description": "Does the key have only read access? Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "bool" + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + }, + "title": { + "description": "Title of the deploy key. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "url": { + "computed": true, + "description": "URL of the deploy key.", + "description_kind": "plain", + "type": "string" + } + }, + "description": "Forgejo repository deploy key resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_gpg_key": { + "block": { + "attributes": { + "armored_public_key": { + "description": "Armored GPG public key. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "can_certify": { + "computed": true, + "description": "Can this key certify.", + "description_kind": "plain", + "type": "bool" + }, + "can_encrypt_comms": { + "computed": true, + "description": "Can this key encrypt communications.", + "description_kind": "plain", + "type": "bool" + }, + "can_encrypt_storage": { + "computed": true, + "description": "Can this key encrypt storage.", + "description_kind": "plain", + "type": "bool" + }, + "can_sign": { + "computed": true, + "description": "Can this key sign.", + "description_kind": "plain", + "type": "bool" + }, + "created_at": { + "computed": true, + "description": "Time at which the GPG key was created.", + "description_kind": "plain", + "type": "string" + }, + "emails": { + "computed": true, + "description": "Emails associated with the GPG key.", + "description_kind": "plain", + "type": [ + "list", + [ + "object", + { + "email": "string", + "verified": "bool" + } + ] + ] + }, + "expires_at": { + "computed": true, + "description": "Time at which the GPG key expires.", + "description_kind": "plain", + "type": "string" + }, + "id": { + "computed": true, + "description": "Numeric identifier of the GPG key.", + "description_kind": "plain", + "type": "number" + }, + "key_id": { + "computed": true, + "description": "ID of the GPG key.", + "description_kind": "plain", + "type": "string" + }, + "primary_key_id": { + "computed": true, + "description": "Primary ID of the GPG key.", + "description_kind": "plain", + "type": "string" + }, + "public_key": { + "computed": true, + "description": "The public key.", + "description_kind": "plain", + "type": "string" + }, + "subkeys": { + "computed": true, + "description": "Subkeys of the GPG key.", + "description_kind": "plain", + "type": [ + "list", + [ + "object", + { + "can_certify": "bool", + "can_encrypt_comms": "bool", + "can_encrypt_storage": "bool", + "can_sign": "bool", + "created_at": "string", + "expires_at": "string", + "id": "number", + "key_id": "string", + "primary_key_id": "string", + "public_key": "string" + } + ] + ] + } + }, + "description": "Forgejo user GPG key resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_organization": { + "block": { + "attributes": { + "avatar_url": { + "computed": true, + "description": "Avatar URL of the organization.", + "description_kind": "plain", + "type": "string" + }, + "description": { + "computed": true, + "description": "Description of the organization.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "full_name": { + "computed": true, + "description": "Full name of the organization.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description": "Numeric identifier of the organization.", + "description_kind": "plain", + "type": "number" + }, + "location": { + "computed": true, + "description": "Location of the organization.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Name of the organization. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "repo_admin_change_team_access": { + "computed": true, + "description": "Whether repository admin can add and remove access for teams. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "visibility": { + "computed": true, + "description": "Visibility of the organization. Possible values are 'public' (default), 'limited', or 'private'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "website": { + "computed": true, + "description": "Website of the organization.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description": "Forgejo organization resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_organization_action_secret": { + "block": { + "attributes": { + "created_at": { + "computed": true, + "description": "Time at which the secret was created.", + "description_kind": "plain", + "type": "string" + }, + "data": { + "description": "Data of the secret.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "name": { + "description": "Name of the secret. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "organization": { + "computed": true, + "description": "Name of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "string" + }, + "organization_id": { + "computed": true, + "description": "Numeric identifier of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "number" + } + }, + "description": "Forgejo organization action secret resource.\n\n**Note**: The authenticated user must be a member of the managed organization(s) or have administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_organization_action_variable": { + "block": { + "attributes": { + "data": { + "description": "Data of the variable.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Name of the variable.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "organization": { + "computed": true, + "description": "Name of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "string" + }, + "organization_id": { + "computed": true, + "description": "Numeric identifier of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "number" + } + }, + "description": "Forgejo organization action variable resource.\n\n**Note**: The authenticated user must be a member of the managed organization(s) or have administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_repository": { + "block": { + "attributes": { + "allow_fast_forward_only_merge": { + "computed": true, + "description": "Allowed to fast-forward-only merge pull requests? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_manual_merge": { + "computed": true, + "description": "Allowed to manually merge pull requests? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_merge_commits": { + "computed": true, + "description": "Allowed to create merge commit? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_rebase": { + "computed": true, + "description": "Allowed to rebase then fast-forward? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_rebase_explicit": { + "computed": true, + "description": "Allowed to rebase then create merge commit? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_rebase_update": { + "computed": true, + "description": "Allowed to update pull request branch by rebase? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_squash_merge": { + "computed": true, + "description": "Allowed to create squash commit? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "archive_on_destroy": { + "computed": true, + "description": "Archive the repo instead of delete?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "archived": { + "computed": true, + "description": "Is the repository archived?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "auth_token": { + "description": "API token for authenticating with migrate / clone URL. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string" + }, + "auto_init": { + "computed": true, + "description": "Whether the repository should be auto-intialized? Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "autodetect_manual_merge": { + "computed": true, + "description": "Auto-detect manual pull request merges? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "avatar_url": { + "computed": true, + "description": "Avatar URL of the repository.", + "description_kind": "plain", + "type": "string" + }, + "clone_addr": { + "computed": true, + "description": "Migrate / clone from URL. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "clone_url": { + "computed": true, + "description": "Clone URL of the repository.", + "description_kind": "plain", + "type": "string" + }, + "created_at": { + "computed": true, + "description": "Time at which the repository was created.", + "description_kind": "plain", + "type": "string" + }, + "default_allow_maintainer_edit": { + "computed": true, + "description": "Allow maintainer edits on pull requests by default? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "default_branch": { + "computed": true, + "description": "Default branch of the repository.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "default_delete_branch_after_merge": { + "computed": true, + "description": "Delete pull request branch after merge by default? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "default_merge_style": { + "computed": true, + "description": "Default merge style of the repository. **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "default_update_style": { + "computed": true, + "description": "Default pull request update style of the repository. **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "computed": true, + "description": "Description of the repository.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "empty": { + "computed": true, + "description": "Is the repository empty?", + "description_kind": "plain", + "type": "bool" + }, + "enable_prune": { + "computed": true, + "description": "Remove obsolete remote-tracking references when mirroring? **Note**: This setting is only effective if `mirror` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "external_tracker": { + "computed": true, + "description": "Settings for external issue tracker. **Note**: This setting is only effective if `has_issues` is `true`.", + "description_kind": "plain", + "nested_type": { + "attributes": { + "external_tracker_format": { + "description": "External issue tracker URL format. Use the placeholders `{user}`, `{repo}` and `{index}` for the username, repository name and issue index.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "external_tracker_regexp_pattern": { + "computed": true, + "description": "External issue tracker issue regular expression.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "external_tracker_style": { + "computed": true, + "description": "External issue tracker number format.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "external_tracker_url": { + "description": "URL of external issue tracker.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "nesting_mode": "single" + }, + "optional": true + }, + "external_wiki": { + "computed": true, + "description": "Settings for external wiki. **Note**: This setting is only effective if `has_wiki` is `true`.", + "description_kind": "plain", + "nested_type": { + "attributes": { + "external_wiki_url": { + "description": "URL of external wiki.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "nesting_mode": "single" + }, + "optional": true + }, + "fork": { + "computed": true, + "description": "Is the repository a fork?", + "description_kind": "plain", + "type": "bool" + }, + "forks_count": { + "computed": true, + "description": "Number of forks of the repository.", + "description_kind": "plain", + "type": "number" + }, + "full_name": { + "computed": true, + "description": "Full name of the repository.", + "description_kind": "plain", + "type": "string" + }, + "gitignores": { + "computed": true, + "description": "Gitignores to use. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "globally_editable_wiki": { + "computed": true, + "description": "Is the repository wiki globally editable? **Note**: This setting is only effective if `has_wiki` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_actions": { + "computed": true, + "description": "Are integrated CI/CD pipelines enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_issues": { + "computed": true, + "description": "Is the repository issue tracker enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_packages": { + "computed": true, + "description": "Is the repository package registry enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_projects": { + "computed": true, + "description": "Are repository projects enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_pull_requests": { + "computed": true, + "description": "Are repository pull requests enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_releases": { + "computed": true, + "description": "Are repository releases enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "has_wiki": { + "computed": true, + "description": "Is the repository wiki enabled?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "html_url": { + "computed": true, + "description": "HTML URL of the repository.", + "description_kind": "plain", + "type": "string" + }, + "id": { + "computed": true, + "description": "Numeric identifier of the repository.", + "description_kind": "plain", + "type": "number" + }, + "ignore_whitespace_conflicts": { + "computed": true, + "description": "Are whitespace conflicts ignored? **Note**: This setting is only effective if `has_pull_requests` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "internal": { + "computed": true, + "description": "Is the repository internal?", + "description_kind": "plain", + "type": "bool" + }, + "internal_tracker": { + "computed": true, + "description": "Settings for built-in issue tracker. **Note**: This setting is only effective if `has_issues` is `true`.", + "description_kind": "plain", + "nested_type": { + "attributes": { + "allow_only_contributors_to_track_time": { + "computed": true, + "description": "Let only contributors track time?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_issue_dependencies": { + "computed": true, + "description": "Enable dependencies for issues and pull requests?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enable_time_tracker": { + "computed": true, + "description": "Enable time tracking?", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "nesting_mode": "single" + }, + "optional": true + }, + "issue_labels": { + "computed": true, + "description": "Issue Label set to use. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "labels": { + "computed": true, + "description": "Whether to migrate labels. Changing this forces a new resource to be created. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "lfs": { + "computed": true, + "description": "Whether to migrate LFS files. Changing this forces a new resource to be created. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "lfs_endpoint": { + "computed": true, + "description": "LFS endpoint to use. Changing this forces a new resource to be created. **Note**: This setting is only effective if `lfs` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "license": { + "computed": true, + "description": "License to use. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "milestones": { + "computed": true, + "description": "Whether to migrate milestones. Changing this forces a new resource to be created. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "mirror": { + "computed": true, + "description": "Is the repository a mirror? Changing this forces a new resource to be created. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "mirror_interval": { + "computed": true, + "description": "Mirror interval of the repository. **Note**: This setting is only effective if `mirror` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "mirror_updated": { + "computed": true, + "description": "Time at which the repository mirror was updated.", + "description_kind": "plain", + "type": "string" + }, + "name": { + "description": "Name of the repository.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "open_issues_count": { + "computed": true, + "description": "Number of open issues of the repository.", + "description_kind": "plain", + "type": "number" + }, + "open_pr_counter": { + "computed": true, + "description": "Number of open pull requests of the repository.", + "description_kind": "plain", + "type": "number" + }, + "owner": { + "computed": true, + "description": "Owner of the repository (user or organization). Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "parent_id": { + "computed": true, + "description": "Numeric identifier of the parent repository.", + "description_kind": "plain", + "type": "number" + }, + "permissions": { + "computed": true, + "description": "Permissions of the repository.", + "description_kind": "plain", + "nested_type": { + "attributes": { + "admin": { + "computed": true, + "description": "Allowed to administer?", + "description_kind": "plain", + "type": "bool" + }, + "pull": { + "computed": true, + "description": "Allowed to pull?", + "description_kind": "plain", + "type": "bool" + }, + "push": { + "computed": true, + "description": "Allowed to push?", + "description_kind": "plain", + "type": "bool" + } + }, + "nesting_mode": "single" + } + }, + "private": { + "computed": true, + "description": "Is the repository private?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "readme": { + "computed": true, + "description": "Readme of the repository to create. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "release_counter": { + "computed": true, + "description": "Number of releases of the repository.", + "description_kind": "plain", + "type": "number" + }, + "service": { + "computed": true, + "description": "Service to migrate from. Changing this forces a new resource to be created. **Note**: This setting is only effective if `clone_addr` is set.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "size": { + "computed": true, + "description": "Size of the repository in KiB.", + "description_kind": "plain", + "type": "number" + }, + "ssh_url": { + "computed": true, + "description": "SSH URL of the repository.", + "description_kind": "plain", + "type": "string" + }, + "stars_count": { + "computed": true, + "description": "Number of stars of the repository.", + "description_kind": "plain", + "type": "number" + }, + "template": { + "computed": true, + "description": "Is the repository a template?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "trust_model": { + "computed": true, + "description": "TrustModel of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "updated_at": { + "computed": true, + "description": "Time at which the repository was updated.", + "description_kind": "plain", + "type": "string" + }, + "watchers_count": { + "computed": true, + "description": "Number of watchers of the repository.", + "description_kind": "plain", + "type": "number" + }, + "website": { + "computed": true, + "description": "Website of the repository.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "wiki_branch": { + "computed": true, + "description": "Branch used for the repository wiki. **Note**: This setting is only effective if `has_wiki` is `true`.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description": "Forgejo repository resource.\n\n**Note**: Managing user repositories requires administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_repository_action_secret": { + "block": { + "attributes": { + "created_at": { + "computed": true, + "description": "Time at which the secret was created.", + "description_kind": "plain", + "type": "string" + }, + "data": { + "description": "Data of the secret.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "name": { + "description": "Name of the secret. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + } + }, + "description": "Forgejo repository action secret resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_repository_action_variable": { + "block": { + "attributes": { + "data": { + "description": "Data of the variable.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Name of the variable.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + } + }, + "description": "Forgejo repository action variable resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_repository_webhook": { + "block": { + "attributes": { + "active": { + "computed": true, + "description": "Boolean indicating if the webhook is active.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "authorization_header": { + "description": "Authorization header to send to the target.", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string" + }, + "branch_filter": { + "computed": true, + "description": "List of allowed branches for push, branch creation and branch deletion events, specified as glob pattern. If empty or *, events for all branches are reported.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "config": { + "description": "Map of configuration settings.", + "description_kind": "plain", + "required": true, + "type": [ + "map", + "string" + ] + }, + "created_at": { + "computed": true, + "description": "Time at which the webhook was created.", + "description_kind": "plain", + "type": "string" + }, + "events": { + "computed": true, + "description": "List of events which trigger the webhook.", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "repository_id": { + "description": "Numeric identifier of the repository. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + }, + "type": { + "description": "Type of webhook. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "updated_at": { + "computed": true, + "description": "Time at which the webhook was updated.", + "description_kind": "plain", + "type": "string" + }, + "webhook_id": { + "computed": true, + "description": "Numeric identifier of the webhook.", + "description_kind": "plain", + "type": "number" + } + }, + "description": "Forgejo repository webhook resource.", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_ssh_key": { + "block": { + "attributes": { + "created_at": { + "computed": true, + "description": "Time at which the SSH key was created.", + "description_kind": "plain", + "type": "string" + }, + "fingerprint": { + "computed": true, + "description": "Fingerprint of the SSH key.", + "description_kind": "plain", + "type": "string" + }, + "key": { + "description": "Armored SSH key. Trailing newlines must be removed (e.g. using trimspace() function). Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "key_id": { + "computed": true, + "description": "Numeric identifier of the SSH key.", + "description_kind": "plain", + "type": "number" + }, + "key_type": { + "computed": true, + "description": "Type of the SSH key.", + "description_kind": "plain", + "type": "string" + }, + "read_only": { + "computed": true, + "description": "Does the key have only read access?", + "description_kind": "plain", + "type": "bool" + }, + "title": { + "description": "Title of the SSH key. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "url": { + "computed": true, + "description": "URL of the SSH key.", + "description_kind": "plain", + "type": "string" + }, + "user": { + "description": "Name of the user. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description": "Forgejo user SSH key resource.\n\n**Note**: Managing user SSH keys requires administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_team": { + "block": { + "attributes": { + "can_create_org_repo": { + "computed": true, + "description": "Can create repositories?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "description": { + "computed": true, + "description": "Description of the team.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description": "Numeric identifier of the team.", + "description_kind": "plain", + "type": "number" + }, + "includes_all_repositories": { + "computed": true, + "description": "Has access to all repositories?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description": "Name of the team.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "organization": { + "computed": true, + "description": "Name of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "string" + }, + "organization_id": { + "computed": true, + "description": "Numeric identifier of the owning organization. Changing this forces a new resource to be created. **Note**: One of `organization` or `organization_id` must be specified.", + "description_kind": "markdown", + "optional": true, + "type": "number" + }, + "permission": { + "computed": true, + "description": "Permissions within the owning organization. **Note**: If you set `admin` or `owner` here, make sure to set the correct `units_map`.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "units_map": { + "description": "Map of access units. **Note**: If the `permission` is `admin` or `owner` all units must be set to `admin` as well.", + "description_kind": "plain", + "required": true, + "type": [ + "map", + "string" + ] + } + }, + "description": "Forgejo team resource.\n\n**Note**: The authenticated user must be a member of the managed organization(s) or have administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + }, + "forgejo_team_member": { + "block": { + "attributes": { + "team_id": { + "description": "Numeric identifier of the team. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "number" + }, + "user": { + "description": "Username of the team member. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description": "Forgejo team member resource.", + "description_kind": "plain" + }, + "version": 0 + }, + "forgejo_user": { + "block": { + "attributes": { + "active": { + "computed": true, + "description": "Is the user active?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "admin": { + "computed": true, + "description": "Is the user an administrator?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_create_organization": { + "computed": true, + "description": "Allow user to create organizations?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_git_hook": { + "computed": true, + "description": "Allow user to create Git hooks?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "allow_import_local": { + "computed": true, + "description": "Allow user to import local repositories?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "avatar_url": { + "computed": true, + "description": "Avatar URL of the user.", + "description_kind": "plain", + "type": "string" + }, + "created_at": { + "computed": true, + "description": "Time at which the user was created.", + "description_kind": "plain", + "type": "string" + }, + "deactivate_on_destroy": { + "computed": true, + "description": "Deactivate the user instead of delete?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "description": { + "computed": true, + "description": "Description of the user.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "email": { + "description": "Email address of the user.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "followers_count": { + "computed": true, + "description": "Number of following users.", + "description_kind": "plain", + "type": "number" + }, + "following_count": { + "computed": true, + "description": "Number of users followed.", + "description_kind": "plain", + "type": "number" + }, + "full_name": { + "computed": true, + "description": "Full name of the user.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "html_url": { + "computed": true, + "description": "URL to the user's profile page.", + "description_kind": "plain", + "type": "string" + }, + "id": { + "computed": true, + "description": "Numeric identifier of the user.", + "description_kind": "plain", + "type": "number" + }, + "language": { + "computed": true, + "description": "Locale of the user.", + "description_kind": "plain", + "type": "string" + }, + "last_login": { + "computed": true, + "description": "Time at which the user last logged in.", + "description_kind": "plain", + "type": "string" + }, + "location": { + "computed": true, + "description": "Location of the user.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "login": { + "description": "Name of the user. Changing this forces a new resource to be created.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "login_name": { + "computed": true, + "description": "Login name of the user.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "max_repo_creation": { + "computed": true, + "description": "Maximum number of repositories user can create. A value of -1 means no limit.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "must_change_password": { + "computed": true, + "description": "Require user to change password?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "password": { + "description": "Password of the user.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "prohibit_login": { + "computed": true, + "description": "Are user logins prohibited?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "restricted": { + "computed": true, + "description": "Is the user restricted?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "send_notify": { + "computed": true, + "description": "Send notification to administrators? Changing this forces a new resource to be created.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "source_id": { + "computed": true, + "description": "Numeric identifier of the user's authentication source.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "starred_repos_count": { + "computed": true, + "description": "Number of starred repositories.", + "description_kind": "plain", + "type": "number" + }, + "visibility": { + "computed": true, + "description": "Visibility of the user. Possible values are 'public' (default), 'limited', or 'private'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "website": { + "computed": true, + "description": "Website of the user.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description": "Forgejo user resource.\n\n**Note**: Managing users requires administrative privileges!", + "description_kind": "markdown" + }, + "version": 0 + } + }, + "source": "svalabs/forgejo", + "version": "1.5.0" +} diff --git a/services/forgejo/schema.nix b/services/forgejo/schema.nix new file mode 100644 index 0000000..5a7cfcb --- /dev/null +++ b/services/forgejo/schema.nix @@ -0,0 +1,6 @@ +# The vendored svalabs/forgejo provider schema, parsed. +# +# Indirection, not decoration: Nix memoizes `import ` but not +# `builtins.readFile`, and `lib.nix` is instantiated once per system per check. +# Refresh with `nix run .#update-provider-schemas`. +builtins.fromJSON (builtins.readFile ./provider-schema.json) From 7295a64b492e2468f3df710f522496963fa19f7a Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:24:17 +0200 Subject: [PATCH 05/18] test(services/forgejo): cover repository tracker and wiki blocks `external_tracker`, `internal_tracker` and `external_wiki` are the only nested blocks the forgejo provider has, and no test or example exercised them -- so a refactor of the resource surface would move them blind. The fixture declares the built-in tracker on one repository and the external tracker plus external wiki on another (a repository routes issues to one tracker or the other, never both), and asserts each block arrived intact through the live API. Assisted-by: Claude:claude-opus-5 --- services/forgejo/checks.nix | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/services/forgejo/checks.nix b/services/forgejo/checks.nix index 3bdbe45..5a766e1 100644 --- a/services/forgejo/checks.nix +++ b/services/forgejo/checks.nix @@ -50,10 +50,35 @@ }; # owner references the managed organization by key -> ordered after it. + # `internal_tracker` is a nested single block (the plugin-framework + # dialect: a plain JSON object, no `[ ... ]` wrapping). repositories.widgets = { owner = "acme"; description = "Widget factory"; private = false; + has_issues = true; + internal_tracker = { + enable_time_tracker = true; + allow_only_contributors_to_track_time = false; + enable_issue_dependencies = true; + }; + }; + + # The other two nested blocks. A repository routes issues either to + # the built-in tracker or to an external one, never both, so they + # need a repository of their own. + repositories.gadgets = { + owner = "acme"; + description = "Gadget factory"; + private = false; + has_issues = true; + has_wiki = true; + external_tracker = { + external_tracker_url = "https://tracker.example.com/acme/gadgets"; + external_tracker_format = "https://tracker.example.com/acme/gadgets/{index}"; + external_tracker_style = "numeric"; + }; + external_wiki.external_wiki_url = "https://wiki.example.com/acme/gadgets"; }; # organization references the managed org by key (string-name ref). @@ -106,6 +131,8 @@ }; testScript = '' + import json + machine.start() # The whole chain must converge at boot with zero manual token handling: @@ -122,6 +149,25 @@ repo = machine.succeed("curl --fail http://localhost:3000/api/v1/repos/acme/widgets") assert '"Widget factory"' in repo, f"repo description not applied: {repo}" + # Nested single blocks reach Forgejo as plain objects. + widgets = json.loads(repo) + assert widgets["internal_tracker"] == { + "enable_time_tracker": True, + "allow_only_contributors_to_track_time": False, + "enable_issue_dependencies": True, + }, f"internal_tracker not applied: {widgets.get('internal_tracker')}" + + gadgets = json.loads(machine.succeed("curl --fail http://localhost:3000/api/v1/repos/acme/gadgets")) + assert gadgets["external_tracker"] == { + "external_tracker_url": "https://tracker.example.com/acme/gadgets", + "external_tracker_format": "https://tracker.example.com/acme/gadgets/{index}", + "external_tracker_style": "numeric", + "external_tracker_regexp_pattern": "", + }, f"external_tracker not applied: {gadgets.get('external_tracker')}" + assert gadgets["external_wiki"] == { + "external_wiki_url": "https://wiki.example.com/acme/gadgets", + }, f"external_wiki not applied: {gadgets.get('external_wiki')}" + # Per-secret indirection: bob's password was supplied as a host file and # must not appear in the generated config; logging in as bob proves the # value still reached Forgejo intact. From dc214ea1dfce4fb773c9b999ff19c6bc1e2fca8f Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:26:36 +0200 Subject: [PATCH 06/18] refactor(services/forgejo): extract test fixtures into fixtures.nix The VM test's `services.forgejo.runtime` block moves to `fixtures.nix`, and the new `forgejo-rendered-fixtures` package renders it through the real option system and renderer. That makes the fixture serve twice: the VM test proves it converges against a live Forgejo, and the rendered `.tf.json` snapshot lets a refactor of the resource surface be checked with a diff. The test derivation is byte-identical either side of this commit. Assisted-by: Claude:claude-opus-5 --- flake.nix | 24 +++++++++ modules/lib/render-fixtures.nix | 44 ++++++++++++++++ services/forgejo/checks.nix | 79 +++-------------------------- services/forgejo/fixtures.nix | 89 +++++++++++++++++++++++++++++++++ 4 files changed, 163 insertions(+), 73 deletions(-) create mode 100644 modules/lib/render-fixtures.nix create mode 100644 services/forgejo/fixtures.nix diff --git a/flake.nix b/flake.nix index b9b7c4d..bf1fed0 100644 --- a/flake.nix +++ b/flake.nix @@ -62,6 +62,29 @@ ) ) (pairingLibs pkgs); + # `-rendered-fixtures`: each pairing's fixtures rendered through the + # real option system and renderer. Build before and after a change to the + # resource surface and diff -- an empty diff proves the wire format is + # untouched. The `baseUrl` here only has to be stable across the two + # builds; the module's own default is what a real system uses. + renderedFixtures = + pkgs: + let + libs = pairingLibs pkgs; + inherit (import ./modules/lib/render-fixtures.nix { inherit pkgs; }) renderFixtures; + urlOption = default: lib.mkOption { inherit default; }; + in + { + forgejo-rendered-fixtures = renderFixtures { + name = "forgejo"; + options = libs.forgejo.resourceOptions // { + baseUrl = urlOption "http://localhost:3000"; + }; + tfConfig = libs.forgejo.forgejoTfConfig; + fixtures = import ./services/forgejo/fixtures.nix; + }; + }; + # `-schema-current`: the authoritative drift check. The eval-time # assertions in `modules/lib/tf-schema.nix` compare version strings; this # one compares content, so a provider that changes a schema without @@ -111,6 +134,7 @@ system: pkgs: lib.mapAttrs (_name: cfg: (exampleSystem system cfg).config.system.build.vm) examples // providerSchemas pkgs + // renderedFixtures pkgs ) inputs.nixpkgs.legacyPackages; # `nix run .#update-provider-schemas` after a nixpkgs bump moves a diff --git a/modules/lib/render-fixtures.nix b/modules/lib/render-fixtures.nix new file mode 100644 index 0000000..664b745 --- /dev/null +++ b/modules/lib/render-fixtures.nix @@ -0,0 +1,44 @@ +# Render a pairing's fixtures through the real option system and renderer, as a +# build artifact. +# +# why: once the option surface is generated from a provider schema, "did this +# change what we send the provider?" can only be answered against rendered +# `.tf.json` -- option definitions alone say nothing about defaults, coercions, +# secret substitution, block wrapping or reference resolution. Build +# `-rendered-fixtures` before and after a change and diff the two; an empty +# diff is proof the wire format is untouched. +# +# The artifact carries no secrets: `File` inputs are host paths, and the +# renderer has already replaced their values with `${var.}` references. +{ pkgs }: +let + inherit (pkgs) lib; +in +{ + # name pairing name, used for the output file + # options the option set a fixture is evaluated against (the pairing's + # `resourceOptions` plus whatever its provider block reads) + # tfConfig the pairing's `TfConfig`: cfg -> { config; credentials; } + # fixtures fixture name -> a `services..runtime` config fragment + renderFixtures = + { + name, + options, + tfConfig, + fixtures, + }: + pkgs.writeText "${name}-rendered-fixtures.json" ( + builtins.toJSON ( + lib.mapAttrs ( + _: fixture: + tfConfig + (lib.evalModules { + modules = [ + { inherit options; } + fixture + ]; + }).config + ) fixtures + ) + ); +} diff --git a/services/forgejo/checks.nix b/services/forgejo/checks.nix index 5a766e1..74399a0 100644 --- a/services/forgejo/checks.nix +++ b/services/forgejo/checks.nix @@ -12,6 +12,9 @@ # per-secret credential indirection — the value is loaded from a host file # and kept out of the generated `.tf.json`. Requires KVM (a NixOS VM test). { pkgs, self }: +let + fixtures = import ./fixtures.nix; +in { forgejo = pkgs.testers.runNixOSTest { name = "declarative-forgejo"; @@ -43,74 +46,8 @@ # declared resource without ever needing to be re-scoped. runtime = { enable = true; - - organizations.acme = { - visibility = "public"; - description = "ACME Corporation"; - }; - - # owner references the managed organization by key -> ordered after it. - # `internal_tracker` is a nested single block (the plugin-framework - # dialect: a plain JSON object, no `[ ... ]` wrapping). - repositories.widgets = { - owner = "acme"; - description = "Widget factory"; - private = false; - has_issues = true; - internal_tracker = { - enable_time_tracker = true; - allow_only_contributors_to_track_time = false; - enable_issue_dependencies = true; - }; - }; - - # The other two nested blocks. A repository routes issues either to - # the built-in tracker or to an external one, never both, so they - # need a repository of their own. - repositories.gadgets = { - owner = "acme"; - description = "Gadget factory"; - private = false; - has_issues = true; - has_wiki = true; - external_tracker = { - external_tracker_url = "https://tracker.example.com/acme/gadgets"; - external_tracker_format = "https://tracker.example.com/acme/gadgets/{index}"; - external_tracker_style = "numeric"; - }; - external_wiki.external_wiki_url = "https://wiki.example.com/acme/gadgets"; - }; - - # organization references the managed org by key (string-name ref). - teams.engineers = { - organization = "acme"; - description = "Engineering"; - units_map = { - "repo.code" = "read"; - }; - }; - organization_action_variables.ci_region = { - organization = "acme"; - data = "eu-west"; - }; - - # repository references the managed repo by key -> emitted as a - # ${forgejo_repository.widgets.id} numeric reference. - repository_action_variables.build_flag = { - repository = "widgets"; - data = "release"; - }; - - # Per-secret indirection: bob's password comes from a host file via - # LoadCredential, so the literal never lands in the generated .tf.json. - # (Same `File` mechanism backs action-secret data, repo - # auth_token, and webhook authorization_header.) - users.bob = { - email = "bob@localhost.localdomain"; - passwordFile = "/etc/forgejo-bob-password"; - must_change_password = false; - }; - }; + } + // fixtures.main; }; virtualisation = { @@ -122,11 +59,7 @@ # user requires write:admin + read:user -- scopes the token already has -- # so activation mustt just apply the new resource with the same token. specialisation.widenScope.configuration = { - services.forgejo.runtime.users.alice = { - email = "alice@localhost.localdomain"; - passwordFile = "/etc/forgejo-alice-password"; - must_change_password = false; - }; + services.forgejo.runtime = fixtures.widenScope; }; }; diff --git a/services/forgejo/fixtures.nix b/services/forgejo/fixtures.nix new file mode 100644 index 0000000..4923363 --- /dev/null +++ b/services/forgejo/fixtures.nix @@ -0,0 +1,89 @@ +# `services.forgejo.runtime` fixtures, shared by the VM test in ./checks.nix and +# by the `forgejo-rendered-fixtures` package. +# +# why the indirection: `forgejo-rendered-fixtures` renders these through the +# real option system and renderer, so the `.tf.json` snapshot that guards +# refactors of the resource surface is produced from exactly the configuration +# the VM test proves converges against a live Forgejo. +{ + # Everything the VM test declares at boot. Spans both reference kinds (a + # by-name organization reference, a numeric by-id repository reference), the + # three nested single blocks, and per-secret `File` indirection. + main = { + organizations.acme = { + visibility = "public"; + description = "ACME Corporation"; + }; + + # owner references the managed organization by key -> ordered after it. + # `internal_tracker` is a nested single block (the plugin-framework dialect: + # a plain JSON object, no `[ ... ]` wrapping). + repositories.widgets = { + owner = "acme"; + description = "Widget factory"; + private = false; + has_issues = true; + internal_tracker = { + enable_time_tracker = true; + allow_only_contributors_to_track_time = false; + enable_issue_dependencies = true; + }; + }; + + # The other two nested blocks. A repository routes issues either to the + # built-in tracker or to an external one, never both, so they need a + # repository of their own. + repositories.gadgets = { + owner = "acme"; + description = "Gadget factory"; + private = false; + has_issues = true; + has_wiki = true; + external_tracker = { + external_tracker_url = "https://tracker.example.com/acme/gadgets"; + external_tracker_format = "https://tracker.example.com/acme/gadgets/{index}"; + external_tracker_style = "numeric"; + }; + external_wiki.external_wiki_url = "https://wiki.example.com/acme/gadgets"; + }; + + # organization references the managed org by key (string-name ref). + teams.engineers = { + organization = "acme"; + description = "Engineering"; + units_map = { + "repo.code" = "read"; + }; + }; + organization_action_variables.ci_region = { + organization = "acme"; + data = "eu-west"; + }; + + # repository references the managed repo by key -> emitted as a + # ${forgejo_repository.widgets.id} numeric reference. + repository_action_variables.build_flag = { + repository = "widgets"; + data = "release"; + }; + + # Per-secret indirection: bob's password comes from a host file via + # LoadCredential, so the literal never lands in the generated .tf.json. + # (Same `File` mechanism backs action-secret data, repo auth_token, + # and webhook authorization_header.) + users.bob = { + email = "bob@localhost.localdomain"; + passwordFile = "/etc/forgejo-bob-password"; + must_change_password = false; + }; + }; + + # What the VM test's `widenScope` specialisation adds on top of `main`. + widenScope = { + users.alice = { + email = "alice@localhost.localdomain"; + passwordFile = "/etc/forgejo-alice-password"; + must_change_password = false; + }; + }; +} From a466e9155509039caaadbfd2458b0555bb267a14 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:36:34 +0200 Subject: [PATCH 07/18] style(modules/lib): apply nixfmt to the schema generator The line escaped the previous commit because treefmt's local cache had already recorded the file as formatted; the `formatting` check builds in a fresh tree and reformats it. Assisted-by: Claude:claude-opus-5 --- modules/lib/tf-schema.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/lib/tf-schema.nix b/modules/lib/tf-schema.nix index d20d5ad..c8234c7 100644 --- a/modules/lib/tf-schema.nix +++ b/modules/lib/tf-schema.nix @@ -131,7 +131,8 @@ in # the type check has to come first: everything below indexes the # schema by it. resourceSchema = - schema.resource_schemas.${o.type} or (throw "${runtimePrefix}: collection '${collection}' models resource `${o.type}`, which provider ${source} ${provider.version} does not have"); + schema.resource_schemas.${o.type} + or (throw "${runtimePrefix}: collection '${collection}' models resource `${o.type}`, which provider ${source} ${provider.version} does not have"); # top-level nodes, and every node by dotted path. tree = conv.settableTree resourceSchema; From f4de973535f4d3149cce2a6aed3f2bd63b961d64 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:36:54 +0200 Subject: [PATCH 08/18] refactor(services/forgejo): derive resourceTypes from the vendored schema The 15 resource specs were hand-transcribed from the provider's documentation: every attribute name, type, optionality and description written out by hand, with nothing checking them against the provider. A provider bump could add, remove or retype an attribute and the only symptom would be an apply-time error on a live host. They are now derived from `services/forgejo/provider-schema.json` by `modules/lib/tf-schema.nix`. What each collection still states by hand is what a schema cannot carry: the Terraform label prefix, which attribute the attrset key fills, the Forgejo token scope, the reference graph between collections, and the NixOS-facing collection description. Everything else -- attribute names, Nix types, required-vs-optional, secrets, block wrapping, descriptions -- comes from the schema, and any drift between the two is now an eval-time error. `nixTfSchema` reaches the module via `_module.args`, injected by the flake: a NixOS module cannot resolve a flake input by path. One deliberate correction: `organization_id` is dropped from `forgejo_team`, `forgejo_organization_action_secret` and `forgejo_organization_action_variable`. The provider requires exactly one of `organization` / `organization_id`, and the existing `organization` reference already accepts both a managed sibling's key and a literal name, so exposing the numeric twin could only be used to violate that constraint. Verified: - `forgejo-rendered-fixtures` builds to the same store path as before the refactor -- the generated `.tf.json` is byte-for-byte unchanged. - `checks.x86_64-linux.forgejo` builds to the same store path as before, so the VM test's whole system closure is unchanged. - Options doc: 169 options before and after, none added or removed, none changed between required and optional. 110 descriptions now come from the provider instead of hand prose. Five types widen: three `int` -> `number` (faithful to the schema's `number`), and `repository_webhooks.config` / `teams.units_map` go from a required `attrsOf str` to a nullable one -- a collection default of `{ }` cannot express "unset", so both stay enforced via `requiredAttrs` instead. Assisted-by: Claude:claude-opus-5 --- flake.nix | 20 +- services/forgejo/lib.nix | 516 +++++++++++++----------------------- services/forgejo/module.nix | 7 +- 3 files changed, 202 insertions(+), 341 deletions(-) diff --git a/flake.nix b/flake.nix index bf1fed0..0262312 100644 --- a/flake.nix +++ b/flake.nix @@ -38,10 +38,22 @@ # the pairings, by service name. each `lib.nix` exposes the packaged # provider and its source address, which is all the schema tooling needs. pairingLibs = pkgs: { - forgejo = import ./services/forgejo/lib.nix { inherit pkgs; }; + forgejo = import ./services/forgejo/lib.nix { + inherit pkgs; + nixTfSchema = inputs.nix-tf-schema; + }; keycloak = import ./services/keycloak/lib.nix { inherit pkgs; }; }; + # A NixOS module cannot reach a flake input by path, so the schema library + # a pairing derives its resource surface from is threaded in as a module + # argument. Wrapping the exported modules keeps that plumbing invisible to + # anyone importing them. + withSchemaLib = module: { + imports = [ module ]; + _module.args.nixTfSchema = inputs.nix-tf-schema; + }; + # `-provider-schema`: the normalized schema of the pinned provider, # extracted in a sandbox (`tofu providers schema -json`). This is the # source for the vendored `services//provider-schema.json`; it is @@ -122,9 +134,9 @@ { # NixOS module entrypoint: enables a Nixpkgs service and reconciles its # runtime state via OpenTofu after the primary unit starts. - nixosModules.default = ./modules; - nixosModules.forgejo = ./services/forgejo/module.nix; - nixosModules.keycloak = ./services/keycloak/module.nix; + nixosModules.default = withSchemaLib ./modules; + nixosModules.forgejo = withSchemaLib ./services/forgejo/module.nix; + nixosModules.keycloak = withSchemaLib ./services/keycloak/module.nix; nixosConfigurations = lib.mapAttrs' ( name: cfg: lib.nameValuePair "example-${name}" (exampleSystem "x86_64-linux" cfg) diff --git a/services/forgejo/lib.nix b/services/forgejo/lib.nix index e8a69a3..ae49a30 100644 --- a/services/forgejo/lib.nix +++ b/services/forgejo/lib.nix @@ -1,24 +1,25 @@ -# forgejo-provider specifics: executor, resource types, provider block. -# shared helpers (option helpers, renderer, reconciler) live in modules/lib. -{ pkgs }: +# forgejo-provider specifics: executor, resource surface, provider block. +# +# The resource surface is *derived* from the vendored provider schema +# (./provider-schema.json, parsed by ./schema.nix) via +# ../../modules/lib/tf-schema.nix. What stays hand-written is only what a schema +# cannot state: the NixOS-facing collection descriptions, the reference graph +# between collections, the Forgejo token scope each resource needs, and the odd +# documented correction. A provider bump that adds, removes or retypes anything +# is then an eval-time error rather than an apply-time surprise. +# +# Shared helpers (option helpers, renderer, reconciler) live in modules/lib. +{ pkgs, nixTfSchema }: let genlib = import ../../modules/lib { inherit pkgs; }; - inherit (genlib) - oStr - oBool - oInt - oListStr - oSub - rStr - rBool - rMapStr - ; + tfSchema = import ../../modules/lib/tf-schema.nix { inherit pkgs nixTfSchema; }; inherit (pkgs) lib; provider = import ./pkg.nix { inherit pkgs; }; providerVersion = provider.version; # provider source address; also keys the vendored provider schema. providerSource = "svalabs/forgejo"; + runtimePrefix = "services.forgejo.runtime"; tokenVar = "forgejo_api_token"; executor = pkgs.opentofu.withPlugins (_: [ provider ]); @@ -64,351 +65,191 @@ let description = "Target user: the key of a managed user, or a literal username."; }; - # The full svalabs/forgejo resource surface. Per resource: - # type the `forgejo_*` resource type - # prefix unique Terraform label prefix - # nameAttr attribute defaulted from the collection key (or null) - # scope Forgejo token scope(s) required to manage the resource - # refs parent links resolved to references against managed siblings - # secrets secret-valued attributes gaining an `File` form - # requiredSecrets secrets the provider requires (one of ``/`File`) - # attrs the settable attributes, each a typed option (no freeform) - resourceTypes = { - organizations = { - type = "forgejo_organization"; - prefix = "org"; - nameAttr = "name"; - scope = "write:organization"; - refs = { }; - description = "Forgejo organizations, keyed by organization name."; - attrs = { - name = oStr "Name of the organization. Defaults to the attribute key."; - description = oStr "Description of the organization."; - full_name = oStr "Full name of the organization."; - location = oStr "Location of the organization."; - repo_admin_change_team_access = oBool "Whether repository admins can add and remove team access."; - visibility = oStr "Visibility: 'public' (default), 'limited', or 'private'."; - website = oStr "Website of the organization."; - }; - }; - users = { - type = "forgejo_user"; - prefix = "user"; - nameAttr = "login"; - # Create goes through the admin API (write:admin); the provider reads the - # user back via /users/search (read:user), a separate scope category. - scope = [ - "write:admin" - "read:user" - ]; - refs = { }; - secrets = [ "password" ]; - requiredSecrets = [ "password" ]; - description = "Forgejo users, keyed by login. Requires administrative privileges."; - attrs = { - login = oStr "Login name of the user. Defaults to the attribute key."; - email = rStr "Email address of the user."; - password = oStr "Password of the user. Prefer `passwordFile` for a real secret."; - full_name = oStr "Full name of the user."; - description = oStr "Description of the user."; - location = oStr "Location of the user."; - website = oStr "Website of the user."; - login_name = oStr "Login name used against the authentication source."; - visibility = oStr "Visibility: 'public' (default), 'limited', or 'private'."; - active = oBool "Is the user active?"; - admin = oBool "Is the user an administrator?"; - allow_create_organization = oBool "Allow the user to create organizations?"; - allow_git_hook = oBool "Allow the user to create Git hooks?"; - allow_import_local = oBool "Allow the user to import local repositories?"; - must_change_password = oBool "Require the user to change password on next login?"; - prohibit_login = oBool "Are user logins prohibited?"; - restricted = oBool "Is the user restricted?"; - send_notify = oBool "Send a notification to administrators on creation?"; - deactivate_on_destroy = oBool "Deactivate the user instead of deleting it?"; - max_repo_creation = oInt "Maximum number of repositories the user can create (-1 = no limit)."; - source_id = oInt "Numeric identifier of the user's authentication source."; + # The provider accepts an owning organization either by name or by numeric id + # and requires exactly one of the two. `orgNameRef` already covers both a + # managed sibling and a literal name, so the numeric twin is dropped rather + # than offered as a way to violate that constraint. + omitOrgId = [ "organization_id" ]; + + # The svalabs/forgejo resource surface. Per collection, only the facts the + # schema does not carry (see modules/lib/tf-schema.nix for the full overlay + # vocabulary): + # type the `forgejo_*` resource type in the schema + # prefix unique Terraform label prefix + # nameAttr attribute defaulted from the collection key (or null) + # scope Forgejo token scope(s) required to manage the resource + # refs parent links resolved to references against managed siblings + # description the collection's NixOS option description + generated = tfSchema.mkResourceTypes { + schema = import ./schema.nix; + inherit provider runtimePrefix; + source = providerSource; + resources = { + organizations = { + type = "forgejo_organization"; + prefix = "org"; + nameAttr = "name"; + scope = "write:organization"; + refs = { }; + description = "Forgejo organizations, keyed by organization name."; }; - }; - repositories = { - type = "forgejo_repository"; - prefix = "repo"; - nameAttr = "name"; - scope = "write:repository"; - refs.owner = { - attr = "owner"; - targets = [ - { - collection = "organizations"; - field = "name"; - } - { - collection = "users"; - field = "login"; - } + users = { + type = "forgejo_user"; + prefix = "user"; + nameAttr = "login"; + # Create goes through the admin API (write:admin); the provider reads the + # user back via /users/search (read:user), a separate scope category. + scope = [ + "write:admin" + "read:user" ]; - managedOnly = false; - required = false; - description = "Repository owner: the key of a managed organization or user, a literal owner name, or null for the authenticated user."; - }; - secrets = [ "auth_token" ]; - description = "Forgejo repositories, keyed by repository name."; - attrs = { - name = oStr "Name of the repository. Defaults to the attribute key."; - description = oStr "Description of the repository."; - website = oStr "Website of the repository."; - default_branch = oStr "Default branch of the repository."; - default_merge_style = oStr "Default merge style (effective only when pull requests are enabled)."; - default_update_style = oStr "Default pull-request update style."; - trust_model = oStr "Trust model of the repository."; - readme = oStr "Readme template to use when auto-initializing."; - gitignores = oStr "Gitignore templates to use when auto-initializing."; - issue_labels = oStr "Issue label set to use when auto-initializing."; - license = oStr "License template to use when auto-initializing."; - wiki_branch = oStr "Branch used for the repository wiki."; - clone_addr = oStr "Migrate / clone source URL (creates a migrated repository)."; - service = oStr "Service to migrate from (effective only when `clone_addr` is set)."; - mirror_interval = oStr "Mirror sync interval (effective only when `mirror` is true)."; - lfs_endpoint = oStr "LFS endpoint to use during migration."; - auth_token = oStr "API token for the migrate / clone URL. Prefer `auth_tokenFile`."; - private = oBool "Is the repository private?"; - template = oBool "Is the repository a template?"; - archived = oBool "Is the repository archived?"; - archive_on_destroy = oBool "Archive the repository instead of deleting it?"; - auto_init = oBool "Auto-initialize the repository?"; - has_actions = oBool "Are integrated CI/CD pipelines enabled?"; - has_issues = oBool "Is the issue tracker enabled?"; - has_packages = oBool "Is the package registry enabled?"; - has_projects = oBool "Are repository projects enabled?"; - has_pull_requests = oBool "Are pull requests enabled?"; - has_releases = oBool "Are releases enabled?"; - has_wiki = oBool "Is the wiki enabled?"; - globally_editable_wiki = oBool "Is the wiki globally editable?"; - allow_merge_commits = oBool "Allow creating merge commits?"; - allow_squash_merge = oBool "Allow squash merges?"; - allow_rebase = oBool "Allow rebase then fast-forward?"; - allow_rebase_explicit = oBool "Allow rebase then create a merge commit?"; - allow_rebase_update = oBool "Allow updating a pull-request branch by rebase?"; - allow_fast_forward_only_merge = oBool "Allow fast-forward-only merges?"; - allow_manual_merge = oBool "Allow marking pull requests manually merged?"; - autodetect_manual_merge = oBool "Auto-detect manual pull-request merges?"; - default_allow_maintainer_edit = oBool "Allow maintainer edits on pull requests by default?"; - default_delete_branch_after_merge = oBool "Delete the branch after merge by default?"; - ignore_whitespace_conflicts = oBool "Ignore whitespace conflicts?"; - enable_prune = oBool "Prune obsolete remote-tracking refs when mirroring?"; - labels = oBool "Migrate labels (effective only when `clone_addr` is set)?"; - lfs = oBool "Migrate LFS files (effective only when `clone_addr` is set)?"; - milestones = oBool "Migrate milestones (effective only when `clone_addr` is set)?"; - mirror = oBool "Is the repository a mirror (effective only when `clone_addr` is set)?"; - external_tracker = oSub { - external_tracker_url = rStr "External issue tracker URL."; - external_tracker_format = rStr "External issue tracker URL format."; - external_tracker_style = oStr "External issue tracker number format style."; - external_tracker_regexp_pattern = oStr "Regular expression matching issue references."; - } "External issue tracker settings (effective only when `has_issues` is true)."; - external_wiki = oSub { - external_wiki_url = rStr "External wiki URL."; - } "External wiki settings (effective only when `has_wiki` is true)."; - internal_tracker = oSub { - enable_time_tracker = oBool "Enable time tracking."; - allow_only_contributors_to_track_time = oBool "Let only contributors track time."; - enable_issue_dependencies = oBool "Enable issue dependencies."; - } "Built-in issue tracker settings (effective only when `has_issues` is true)."; + refs = { }; + description = "Forgejo users, keyed by login. Requires administrative privileges."; }; - }; - teams = { - type = "forgejo_team"; - prefix = "team"; - nameAttr = "name"; - scope = "write:organization"; - refs.organization = orgNameRef; - requiredAttrs = [ "units_map" ]; - description = "Forgejo organization teams, keyed by team name."; - attrs = { - name = oStr "Name of the team. Defaults to the attribute key."; - units_map = rMapStr "Map of access units to permission level (e.g. { \"repo.code\" = \"read\"; })."; - description = oStr "Description of the team."; - permission = oStr "Permission within the organization. If 'admin' or 'owner', set every `units_map` unit to 'admin' too."; - can_create_org_repo = oBool "Can the team create organization repositories?"; - includes_all_repositories = oBool "Does the team have access to all repositories?"; - }; - }; - team_members = { - type = "forgejo_team_member"; - prefix = "team_member"; - nameAttr = null; - scope = "write:organization"; - refs = { - team = { - attr = "team_id"; + repositories = { + type = "forgejo_repository"; + prefix = "repo"; + nameAttr = "name"; + scope = "write:repository"; + refs.owner = { + attr = "owner"; targets = [ { - collection = "teams"; - field = "id"; + collection = "organizations"; + field = "name"; + } + { + collection = "users"; + field = "login"; } ]; - managedOnly = true; - required = true; - description = "Key of the managed team (services.forgejo.runtime.teams.) the member is added to."; + managedOnly = false; + required = false; + description = "Repository owner: the key of a managed organization or user, a literal owner name, or null for the authenticated user."; }; - user = userRef; + description = "Forgejo repositories, keyed by repository name."; }; - description = "Forgejo team memberships, keyed by an arbitrary label."; - attrs = { }; - }; - collaborators = { - type = "forgejo_collaborator"; - prefix = "collab"; - nameAttr = null; - scope = "write:repository"; - refs = { - repository = repoRef; - user = userRef; + teams = { + type = "forgejo_team"; + prefix = "team"; + nameAttr = "name"; + scope = "write:organization"; + refs.organization = orgNameRef; + omit = omitOrgId; + description = "Forgejo organization teams, keyed by team name."; }; - description = "Forgejo repository collaborators, keyed by an arbitrary label."; - attrs = { - permission = rStr "Permission of the collaborator: 'read', 'write', or 'admin'."; + team_members = { + type = "forgejo_team_member"; + prefix = "team_member"; + nameAttr = null; + scope = "write:organization"; + refs = { + team = { + attr = "team_id"; + targets = [ + { + collection = "teams"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed team (services.forgejo.runtime.teams.) the member is added to."; + }; + user = userRef; + }; + description = "Forgejo team memberships, keyed by an arbitrary label."; }; - }; - repository_webhooks = { - type = "forgejo_repository_webhook"; - prefix = "repo_webhook"; - nameAttr = null; - scope = "write:repository"; - refs.repository = repoRef; - requiredAttrs = [ "config" ]; - secrets = [ "authorization_header" ]; - description = "Forgejo repository webhooks, keyed by an arbitrary label."; - attrs = { - type = rStr "Type of webhook (e.g. 'forgejo', 'gitea', 'slack')."; - config = rMapStr "Map of webhook configuration settings (e.g. url, content_type)."; - events = oListStr "Events that trigger the webhook."; - branch_filter = oStr "Glob of branches the webhook reports on (empty or '*' = all)."; - active = oBool "Is the webhook active?"; - authorization_header = oStr "Authorization header sent to the target. Prefer `authorization_headerFile`."; + collaborators = { + type = "forgejo_collaborator"; + prefix = "collab"; + nameAttr = null; + scope = "write:repository"; + refs = { + repository = repoRef; + user = userRef; + }; + description = "Forgejo repository collaborators, keyed by an arbitrary label."; }; - }; - branch_protections = { - type = "forgejo_branch_protection"; - prefix = "branch_protection"; - nameAttr = null; - scope = "write:repository"; - refs.repository = repoRef; - description = "Forgejo branch protections, keyed by an arbitrary label."; - attrs = { - branch_name = rStr "Name of the branch (or glob) to protect."; - required_approvals = oInt "Number of required approvals."; - protected_file_patterns = oStr "Protected file patterns (semicolon-separated)."; - unprotected_file_patterns = oStr "Unprotected file patterns (semicolon-separated)."; - enable_push = oBool "Allow pushing to the branch?"; - enable_push_whitelist = oBool "Restrict push to whitelisted users/teams?"; - enable_merge_whitelist = oBool "Restrict merge to whitelisted users/teams?"; - enable_approvals_whitelist = oBool "Restrict approvals to whitelisted users/teams?"; - enable_status_check = oBool "Require status checks?"; - block_on_official_review_requests = oBool "Block merge on official review requests?"; - block_on_outdated_branch = oBool "Block merge if the pull request is outdated?"; - block_on_rejected_reviews = oBool "Block merge on rejected reviews?"; - dismiss_stale_approvals = oBool "Dismiss stale approvals?"; - require_signed_commits = oBool "Require signed commits?"; - push_whitelist_deploy_keys = oBool "Allow whitelisted deploy keys to push?"; - push_whitelist_usernames = oListStr "Users whitelisted for pushing."; - push_whitelist_teams = oListStr "Teams whitelisted for pushing."; - merge_whitelist_usernames = oListStr "Users whitelisted for merging."; - merge_whitelist_teams = oListStr "Teams whitelisted for merging."; - approvals_whitelist_usernames = oListStr "Users whitelisted for reviewing."; - approvals_whitelist_teams = oListStr "Teams whitelisted for reviewing."; - status_check_contexts = oListStr "Status check patterns required to pass."; + branch_protections = { + type = "forgejo_branch_protection"; + prefix = "branch_protection"; + nameAttr = null; + scope = "write:repository"; + refs.repository = repoRef; + description = "Forgejo branch protections, keyed by an arbitrary label."; }; - }; - deploy_keys = { - type = "forgejo_deploy_key"; - prefix = "deploy_key"; - nameAttr = null; - scope = "write:repository"; - refs.repository = repoRef; - description = "Forgejo repository deploy keys, keyed by an arbitrary label."; - attrs = { - key = rStr "Armored SSH public key (no trailing newline)."; - title = rStr "Title of the deploy key."; - read_only = rBool "Does the key have read-only access?"; + deploy_keys = { + type = "forgejo_deploy_key"; + prefix = "deploy_key"; + nameAttr = null; + scope = "write:repository"; + refs.repository = repoRef; + description = "Forgejo repository deploy keys, keyed by an arbitrary label."; }; - }; - repository_action_secrets = { - type = "forgejo_repository_action_secret"; - prefix = "repo_action_secret"; - nameAttr = "name"; - scope = "write:repository"; - refs.repository = repoRef; - secrets = [ "data" ]; - requiredSecrets = [ "data" ]; - description = "Forgejo repository Actions secrets, keyed by secret name."; - attrs = { - name = oStr "Name of the secret. Defaults to the attribute key."; - data = oStr "Value of the secret. Prefer `dataFile` to keep it out of the world-readable store."; + repository_webhooks = { + type = "forgejo_repository_webhook"; + prefix = "repo_webhook"; + nameAttr = null; + scope = "write:repository"; + refs.repository = repoRef; + description = "Forgejo repository webhooks, keyed by an arbitrary label."; }; - }; - repository_action_variables = { - type = "forgejo_repository_action_variable"; - prefix = "repo_action_var"; - nameAttr = "name"; - scope = "write:repository"; - refs.repository = repoRef; - description = "Forgejo repository Actions variables, keyed by variable name."; - attrs = { - name = oStr "Name of the variable. Defaults to the attribute key."; - data = rStr "Value of the variable."; + repository_action_secrets = { + type = "forgejo_repository_action_secret"; + prefix = "repo_action_secret"; + nameAttr = "name"; + scope = "write:repository"; + refs.repository = repoRef; + description = "Forgejo repository Actions secrets, keyed by secret name."; }; - }; - organization_action_secrets = { - type = "forgejo_organization_action_secret"; - prefix = "org_action_secret"; - nameAttr = "name"; - scope = "write:organization"; - refs.organization = orgNameRef; - secrets = [ "data" ]; - requiredSecrets = [ "data" ]; - description = "Forgejo organization Actions secrets, keyed by secret name."; - attrs = { - name = oStr "Name of the secret. Defaults to the attribute key."; - data = oStr "Value of the secret. Prefer `dataFile` to keep it out of the world-readable store."; + repository_action_variables = { + type = "forgejo_repository_action_variable"; + prefix = "repo_action_var"; + nameAttr = "name"; + scope = "write:repository"; + refs.repository = repoRef; + description = "Forgejo repository Actions variables, keyed by variable name."; }; - }; - organization_action_variables = { - type = "forgejo_organization_action_variable"; - prefix = "org_action_var"; - nameAttr = "name"; - scope = "write:organization"; - refs.organization = orgNameRef; - description = "Forgejo organization Actions variables, keyed by variable name."; - attrs = { - name = oStr "Name of the variable. Defaults to the attribute key."; - data = rStr "Value of the variable."; + organization_action_secrets = { + type = "forgejo_organization_action_secret"; + prefix = "org_action_secret"; + nameAttr = "name"; + scope = "write:organization"; + refs.organization = orgNameRef; + omit = omitOrgId; + description = "Forgejo organization Actions secrets, keyed by secret name."; }; - }; - ssh_keys = { - type = "forgejo_ssh_key"; - prefix = "ssh_key"; - nameAttr = null; - scope = "write:admin"; - refs.user = userRef; - description = "Forgejo user SSH keys, keyed by an arbitrary label. Requires administrative privileges."; - attrs = { - key = rStr "Armored SSH public key (no trailing newline)."; - title = rStr "Title of the SSH key."; + organization_action_variables = { + type = "forgejo_organization_action_variable"; + prefix = "org_action_var"; + nameAttr = "name"; + scope = "write:organization"; + refs.organization = orgNameRef; + omit = omitOrgId; + description = "Forgejo organization Actions variables, keyed by variable name."; }; - }; - gpg_keys = { - type = "forgejo_gpg_key"; - prefix = "gpg_key"; - nameAttr = null; - scope = "write:user"; - refs = { }; - description = "Forgejo GPG keys for the authenticated user, keyed by an arbitrary label."; - attrs = { - armored_public_key = rStr "Armored GPG public key."; + ssh_keys = { + type = "forgejo_ssh_key"; + prefix = "ssh_key"; + nameAttr = null; + scope = "write:admin"; + refs.user = userRef; + description = "Forgejo user SSH keys, keyed by an arbitrary label. Requires administrative privileges."; + }; + gpg_keys = { + type = "forgejo_gpg_key"; + prefix = "gpg_key"; + nameAttr = null; + scope = "write:user"; + refs = { }; + description = "Forgejo GPG keys for the authenticated user, keyed by an arbitrary label."; }; }; }; + inherit (generated) resourceTypes; + # union of token scopes for the declared resource collections (least- # privilege set for the config). currently dormant: the bootstrap mints # a maximally-scoped ("all") token to avoid having to re-mint. switch @@ -427,10 +268,10 @@ let resourceTypes providerVersion providerSource + runtimePrefix tokenVar ; providerName = "forgejo"; - runtimePrefix = "services.forgejo.runtime"; providerBlock = cfg: { host = cfg.baseUrl; api_token = "\${var.${tokenVar}}"; @@ -445,6 +286,9 @@ in requiredScopes forgejoTfConfig ; + # the generator's drift assertions, for the pairing's checks to force + # independently of any particular configuration. + inherit (generated) checks; resourceOptions = genlib.resourceOptions resourceTypes; mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; }); } diff --git a/services/forgejo/module.nix b/services/forgejo/module.nix index c44f6bf..bf12341 100644 --- a/services/forgejo/module.nix +++ b/services/forgejo/module.nix @@ -5,10 +5,15 @@ # the live Forgejo API once `forgejo.service` is up. Unless `tokenFile` is set, # it also bootstraps the admin API token the reconciler needs via a companion # oneshot (declarative-forgejo-token.service). +# +# `nixTfSchema` is the schema-conversion library the resource surface is derived +# from; the flake injects it via `_module.args`, since a NixOS module cannot +# reach a flake input by path. { config, lib, pkgs, + nixTfSchema, ... }: let @@ -22,7 +27,7 @@ let cfg = config.services.forgejo.runtime; forgejo = config.services.forgejo; - tflib = import ./lib.nix { inherit pkgs; }; + tflib = import ./lib.nix { inherit pkgs nixTfSchema; }; defaultBaseUrl = "http://localhost:${toString forgejo.settings.server.HTTP_PORT}"; From b51d1b9cae7cabffee0337e9acbcec76ead96c44 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:40:53 +0200 Subject: [PATCH 09/18] feat(services/forgejo): add schema coverage and options-doc checks Three gaps in what `nix flake check` proved after the surface became schema-derived: - The generator's drift assertions only fired as a side effect of forcing `resourceTypes`, so drift surfaced as whichever VM test or example happened to evaluate first -- a confusing place to read the error. - Nothing recorded what the pairing covers. "Is this resource modelled, and how much of it?" could only be answered by reading `lib.nix`. - Nothing rendered the user-facing option surface, so an API change could only be reviewed as a diff of generator input, not of generator output. `-schema-coverage` builds the coverage table from the generator's new `coverage` output, forcing every assertion on its own along the way, and is the source for the pairing README's resource table (and, once a pairing has one, its `unsupported` list). `-options-doc` renders the option surface with `nixosOptionsDoc`; `warningsAreErrors` stays on, so an option without a description fails the check. Both are generic over pairings: keycloak already gets an options doc, and picks up a coverage report as soon as it exports `coverage`. Assisted-by: Claude:claude-opus-5 --- flake.nix | 44 ++++++++++++++++++++++++ modules/lib/schema-report.nix | 64 +++++++++++++++++++++++++++++++++++ modules/lib/tf-schema.nix | 30 +++++++++++++--- services/forgejo/lib.nix | 6 ++-- 4 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 modules/lib/schema-report.nix diff --git a/flake.nix b/flake.nix index 0262312..877a8ec 100644 --- a/flake.nix +++ b/flake.nix @@ -97,6 +97,48 @@ }; }; + # `-schema-coverage`: the pairing's coverage table. Building it forces + # the generator's drift assertions on their own, so schema drift fails a + # check that names it rather than whichever VM test happens to eval first. + # + # A pairing opts in by exporting `coverage`, i.e. by deriving its resource + # surface from the vendored schema. + schemaCoverage = + pkgs: + let + inherit (import ./modules/lib/schema-report.nix { inherit pkgs; }) mkCoverageReport; + in + lib.mapAttrs' ( + svc: l: + lib.nameValuePair "${svc}-schema-coverage" (mkCoverageReport { + name = svc; + inherit (l) coverage; + }) + ) (lib.filterAttrs (_: l: l ? coverage) (pairingLibs pkgs)); + + # `-options-doc`: the pairing's user-facing option surface as + # `options.json`. Build it before and after a change and diff the two -- + # that is the record of what the API gained, lost or retyped, which + # rendered `.tf.json` alone cannot show (an option nobody sets renders to + # nothing either way). + optionsDocs = + pkgs: + lib.mapAttrs' ( + svc: l: + lib.nameValuePair "${svc}-options-doc" + (pkgs.nixosOptionsDoc { + inherit + ( + (lib.evalModules { + modules = [ { options.services.${svc}.runtime = l.resourceOptions; } ]; + }) + ) + options + ; + warningsAreErrors = true; + }).optionsJSON + ) (pairingLibs pkgs); + # `-schema-current`: the authoritative drift check. The eval-time # assertions in `modules/lib/tf-schema.nix` compare version strings; this # one compares content, so a provider that changes a schema without @@ -188,6 +230,8 @@ lib.nameValuePair "example-${name}" (exampleSystem system cfg).config.system.build.toplevel ) examples) // schemaChecks pkgs + // schemaCoverage pkgs + // optionsDocs pkgs // { formatting = inputs.self.formatter.${system}.check inputs.self; } diff --git a/modules/lib/schema-report.nix b/modules/lib/schema-report.nix new file mode 100644 index 0000000..e4b26b8 --- /dev/null +++ b/modules/lib/schema-report.nix @@ -0,0 +1,64 @@ +# Render a pairing's schema coverage as a markdown table, as a build artifact. +# +# why: two jobs at once. Building it forces the generator's drift assertions on +# their own, so `nix flake check` reports schema drift as a named failing check +# instead of burying it inside a VM test. And the output is the review artifact +# for "what does this pairing actually cover?" -- which resources are modelled, +# which are deliberately not, and how much of each resource's surface survives +# the overlay's corrections. +{ pkgs }: +let + inherit (pkgs) lib; + + # markdown table cell for a list: `a`, `b` -- or an em dash when empty. + cell = xs: if xs == [ ] then "--" else lib.concatMapStringsSep ", " (x: "`${x}`") xs; +in +{ + # name pairing name, used for the output file and the heading + # coverage the `coverage` attrset returned by ./tf-schema.nix + mkCoverageReport = + { name, coverage }: + let + modelled = lib.length (lib.attrNames coverage.collections); + + row = + collection: c: + "| `${collection}` | `${c.type}` | ${toString c.options} of ${toString c.attributes} | ${cell c.secrets} | ${cell c.refs} | ${cell c.omitted} |"; + + unsupportedSection = + if coverage.unsupported == { } then + "Every resource the provider offers is modelled.\n" + else + '' + | Provider resource | Reason | + | --- | --- | + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (type: reason: "| `${type}` | ${reason} |") coverage.unsupported + )} + ''; + in + pkgs.writeText "${name}-schema-coverage.md" '' + # ${name} runtime resource coverage + + Derived from `services/${name}/provider-schema.json`; regenerated by + `nix build .#checks..${name}-schema-coverage`. Do not edit. + + Provider `${coverage.source}` ${coverage.version}: ${toString modelled} of ${toString coverage.schemaResources} resources modelled. + + `Options` counts a collection's top-level options against the settable + top-level attributes the provider schema declares for it; the two differ + exactly by the attributes consumed by references and by `Omitted`. Nested + block attributes are options of their own submodule and are not counted + here. + + ## Modelled + + | `${coverage.runtimePrefix}.` | Provider resource | Options | Secrets | References | Omitted | + | --- | --- | --- | --- | --- | --- | + ${lib.concatStringsSep "\n" (lib.mapAttrsToList row coverage.collections)} + + ## Not modelled + + ${unsupportedSection} + ''; +} diff --git a/modules/lib/tf-schema.nix b/modules/lib/tf-schema.nix index c8234c7..556280f 100644 --- a/modules/lib/tf-schema.nix +++ b/modules/lib/tf-schema.nix @@ -90,9 +90,10 @@ in requiredAttrs extra non-empty checks extraAttrs last-resort typed overrides; keys must name real attributes - Returns `{ resourceTypes; checks; }`. `checks` is a list of null-or-throw, - `deepSeq`'d into `resourceTypes`, so merely forcing the latter fires every - assertion -- a pairing cannot use the surface without also checking it. + Returns `{ resourceTypes; checks; coverage; }`. `checks` is a list of + null-or-throw, `deepSeq`'d into `resourceTypes`, so merely forcing the + latter fires every assertion -- a pairing cannot use the surface without + also checking it. `coverage` is report data for `-schema-coverage`. */ mkResourceTypes = { @@ -355,9 +356,19 @@ in ; attrs = mkOptions "" tree // o.extraAttrs; }; + + # what the pairing covers, for the generated coverage report. + coverage = { + inherit (o) type prefix description; + options = length (attrNames spec.attrs); + attributes = length (attrNames tree); + omitted = sortStrings o.omit; + refs = sortStrings (attrNames o.refs); + inherit secrets blockAttrs; + }; in { - inherit spec checks; + inherit spec checks coverage; }; built = lib.mapAttrs mkOne resources; @@ -420,5 +431,16 @@ in # generated options without also having checked them for drift. resourceTypes = builtins.deepSeq checks (lib.mapAttrs (_: r: r.spec) built); inherit checks; + + coverage = builtins.deepSeq checks { + inherit + source + runtimePrefix + unsupported + ; + inherit (provider) version; + schemaResources = length schemaTypes; + collections = lib.mapAttrs (_: r: r.coverage) built; + }; }; } diff --git a/services/forgejo/lib.nix b/services/forgejo/lib.nix index ae49a30..7539898 100644 --- a/services/forgejo/lib.nix +++ b/services/forgejo/lib.nix @@ -286,9 +286,9 @@ in requiredScopes forgejoTfConfig ; - # the generator's drift assertions, for the pairing's checks to force - # independently of any particular configuration. - inherit (generated) checks; + # the generator's drift assertions and coverage data, for the pairing's checks + # to force independently of any particular configuration. + inherit (generated) checks coverage; resourceOptions = genlib.resourceOptions resourceTypes; mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; }); } From d596dabf559d3a0a2672a1fb12bfe48526dea6e7 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:41:35 +0200 Subject: [PATCH 10/18] docs(services/forgejo): document schema-derived resources The README read as though the option surface were hand-maintained, and gave no answer to "how do I move to a newer provider?" -- which is now a mechanical procedure with a check that reports exactly what changed. Points the resource table at `forgejo-options-doc` and `forgejo-schema-coverage` as the authoritative lists, explains why `organization_id` has no option, and adds a "Provider updates" section covering the bump procedure, what drift the check reports, and `forceOptional` as the release valve for a newly-required attribute. Assisted-by: Claude:claude-opus-5 --- services/forgejo/README.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/services/forgejo/README.md b/services/forgejo/README.md index 954700f..78f93da 100644 --- a/services/forgejo/README.md +++ b/services/forgejo/README.md @@ -49,6 +49,10 @@ an unknown name or wrong type is a build error). Parent links name another managed entry by its key, which resolves to a correctly ordered Terraform reference. +Those options are **derived from the provider's own schema** rather than +hand-written, so they track the pinned provider exactly — see +[Provider updates](#provider-updates). + ### Organizations, repositories, and teams Create an organization, a repository it owns, and a team inside it. `owner` and @@ -181,7 +185,13 @@ Plus one collection option per provider resource (next section). ## Resources Every [`svalabs/forgejo`][provider] resource is exposed as a collection keyed by -an arbitrary handle: +an arbitrary handle. What each collection accepts comes from the vendored +provider schema, not from this table: the authoritative option list is + +```sh +nix build .#checks.x86_64-linux.forgejo-options-doc # every option, typed +nix build .#checks.x86_64-linux.forgejo-schema-coverage # what is covered +``` | Option | `forgejo_*` resource | Key defaults | Reference inputs | | ------------------------------- | ------------------------------ | ------------ | ---------------------------------- | @@ -201,6 +211,32 @@ an arbitrary handle: | `ssh_keys` | `ssh_key` | — | `user` → user (requires admin) | | `gpg_keys` | `gpg_key` | — | — | +The provider takes an owning organization either by name or by numeric +`organization_id` and requires exactly one of the two. Only the name form is +exposed, since the `organization` reference above already accepts both a managed +organization's key and a literal name. + +## Provider updates + +The option surface is generated from `provider-schema.json`, a normalized dump +of the pinned provider's schema, committed next to `pkg.nix`. To move to a newer +provider, bump `rev`/`hash`/`vendorHash` in `pkg.nix`, then: + +```sh +nix run .#update-provider-schemas +nix flake check +``` + +The check names every difference the bump introduces: resources added or +removed, attributes added, removed or retyped, and any correction in `lib.nix` +that no longer matches the schema. A new resource must either be modelled or +listed in the pairing's `unsupported` set with a reason — it cannot be ignored. + +An upstream attribute that becomes **required** turns into a required option, +which fails evaluation for configurations that never set it. That is usually the +right signal, but `forceOptional` in the collection's overlay is the release +valve when it is not. + ## Security note Secret-valued _resource_ attributes — `password` (`users`), `data` From a427db68788150efbde16f6cef9aef52a575187d Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:42:17 +0200 Subject: [PATCH 11/18] feat(services/keycloak): vendor the provider schema Commits the normalized schema of the pinned keycloak/keycloak provider, so the pairing can derive its resource surface from the provider's own declaration instead of a hand transcription. Vendored rather than extracted at eval time: the flake evaluates for aarch64-linux as well, and IFD would mean running a foreign-arch provider binary during evaluation. Provider schemas are platform-independent, so one committed file per provider version is correct. Activates `keycloak-schema-current`, which rebuilds the schema from the pinned provider and fails if the committed copy differs -- catching a provider that changes its schema without changing its version. Assisted-by: Claude:claude-opus-5 --- services/keycloak/provider-schema.json | 8721 ++++++++++++++++++++++++ services/keycloak/schema.nix | 6 + 2 files changed, 8727 insertions(+) create mode 100644 services/keycloak/provider-schema.json create mode 100644 services/keycloak/schema.nix diff --git a/services/keycloak/provider-schema.json b/services/keycloak/provider-schema.json new file mode 100644 index 0000000..b321733 --- /dev/null +++ b/services/keycloak/provider-schema.json @@ -0,0 +1,8721 @@ +{ + "format_version": "1.0", + "resource_schemas": { + "keycloak_attribute_importer_identity_provider_mapper": { + "block": { + "attributes": { + "attribute_friendly_name": { + "description": "Attribute Friendly Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attribute_name": { + "description": "Attribute Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "claim_name": { + "description": "Claim Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_attribute": { + "description": "User Attribute", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_attribute_to_role_identity_provider_mapper": { + "block": { + "attributes": { + "attribute_friendly_name": { + "description": "Attribute Friendly Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attribute_name": { + "description": "Attribute Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attribute_value": { + "description": "Attribute Value", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "claim_name": { + "description": "OIDC Claim Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "claim_value": { + "description": "OIDC Claim Value", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role": { + "description": "Role Name", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_authentication_bindings": { + "block": { + "attributes": { + "browser_flow": { + "computed": true, + "description": "Which flow should be used for BrowserFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_authentication_flow": { + "computed": true, + "description": "Which flow should be used for ClientAuthenticationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "direct_grant_flow": { + "computed": true, + "description": "Which flow should be used for DirectGrantFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "docker_authentication_flow": { + "computed": true, + "description": "Which flow should be used for DockerAuthenticationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "first_broker_login_flow": { + "computed": true, + "description": "Which flow should be used for FirstBrokerLoginFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "registration_flow": { + "computed": true, + "description": "Which flow should be used for RegistrationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "reset_credentials_flow": { + "computed": true, + "description": "Which flow should be used for ResetCredentialsFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_authentication_execution": { + "block": { + "attributes": { + "authenticator": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "parent_flow_alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "requirement": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_authentication_execution_config": { + "block": { + "attributes": { + "alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "config": { + "description_kind": "plain", + "required": true, + "type": [ + "map", + "string" + ] + }, + "execution_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_authentication_flow": { + "block": { + "attributes": { + "alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_authentication_subflow": { + "block": { + "attributes": { + "alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "authenticator": { + "description": "Might be needed to be set with certain custom subflow with specific authenticator, in general this will remain empty", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "parent_flow_alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "provider_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "requirement": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_custom_identity_provider_mapper": { + "block": { + "attributes": { + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "identity_provider_mapper": { + "description": "IDP Mapper Type", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_custom_user_federation": { + "block": { + "attributes": { + "cache_policy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "changed_sync_period": { + "description": "How frequently Keycloak should sync changed users, in seconds. Omit this property to disable periodic changed users sync.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "enabled": { + "description": "When false, this provider will not be used when performing queries for users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "full_sync_period": { + "description": "How frequently Keycloak should sync all users, in seconds. Omit this property to disable periodic full sync.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of the provider when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "parent_id": { + "computed": true, + "description": "The parent_id of the generated component. will use realm_id if not specified.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "priority": { + "description": "Priority of this provider when looking up users. Lower values are first.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "provider_id": { + "description": "The unique ID of the custom provider, specified in the `getId` implementation for the UserStorageProviderFactory interface", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm (name) this provider will provide user federation for.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_default_groups": { + "block": { + "attributes": { + "group_ids": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_default_roles": { + "block": { + "attributes": { + "default_roles": { + "description": "Realm level roles (name) assigned to new users.", + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_generic_client_protocol_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "config": { + "description_kind": "plain", + "required": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "protocol": { + "description": "The protocol of the client (openid-connect / saml).", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "protocol_mapper": { + "description": "The type of the protocol mapper.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "deprecated": true, + "deprecation_message": "please use keycloak_generic_protocol_mapper instead", + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_generic_client_role_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The destination client of the role. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The destination client scope of the role. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_id": { + "description": "Id of the role to assign", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "deprecated": true, + "deprecation_message": "please use keycloak_generic_role_mapper instead", + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_generic_protocol_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "config": { + "description_kind": "plain", + "required": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "protocol": { + "description": "The protocol of the client (openid-connect / saml).", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "protocol_mapper": { + "description": "The type of the protocol mapper.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_generic_role_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The destination client of the role. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The destination client scope of the role. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_id": { + "description": "Id of the role to assign", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_group": { + "block": { + "attributes": { + "attributes": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "organization_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "parent_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "path": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_group_memberships": { + "block": { + "attributes": { + "group_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "members": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_group_permissions": { + "block": { + "attributes": { + "authorization_resource_server_id": { + "computed": true, + "description": "Resource server id representing the realm management client on which this permission is managed", + "description_kind": "plain", + "type": "string" + }, + "enabled": { + "computed": true, + "description_kind": "plain", + "type": "bool" + }, + "group_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "manage_members_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "manage_membership_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "manage_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "view_members_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "view_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_group_roles": { + "block": { + "attributes": { + "exhaustive": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "group_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_ids": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_hardcoded_attribute_identity_provider_mapper": { + "block": { + "attributes": { + "attribute_name": { + "description": "OIDC Claim", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attribute_value": { + "description": "User Attribute", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_session": { + "description": "Is Attribute Related To a User Session", + "description_kind": "plain", + "required": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_hardcoded_attribute_mapper": { + "block": { + "attributes": { + "attribute_name": { + "description": "Name of the user schema attribute", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "attribute_value": { + "description": "Value of the attribute. You can hardcode any value like 'foo'", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_hardcoded_group_identity_provider_mapper": { + "block": { + "attributes": { + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "group": { + "description": "Group Name", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_hardcoded_role_identity_provider_mapper": { + "block": { + "attributes": { + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role": { + "description": "Role Name", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_identity_provider_token_exchange_scope_permission": { + "block": { + "attributes": { + "authorization_idp_resource_id": { + "computed": true, + "description": "Resource id representing the identity provider, this automatically created by keycloak", + "description_kind": "plain", + "type": "string" + }, + "authorization_resource_server_id": { + "computed": true, + "description": "Resource server id representing the realm management client on which this permission is managed", + "description_kind": "plain", + "type": "string" + }, + "authorization_token_exchange_scope_permission_id": { + "computed": true, + "description": "Permission id representing the Permission with scope 'Token Exchange' and the resource 'authorization_idp_resource_id', this automatically created by keycloak, the policy id will be set on this permission", + "description_kind": "plain", + "type": "string" + }, + "clients": { + "description": "Ids of the clients for which a policy will be created and set on scope based token exchange permission", + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policy_id": { + "computed": true, + "description": "Policy id that will be set on the scope based token exchange permission automatically created by enabling permissions on the reference identity provider", + "description_kind": "plain", + "type": "string" + }, + "policy_type": { + "description": "Type of policy that is created. At the moment only 'client' type is supported", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_kubernetes_identity_provider": { + "block": { + "attributes": { + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_name": { + "description": "Friendly name for Identity Providers.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "computed": true, + "description": "This is always set to true for Kubernetes identity provider.", + "description_kind": "plain", + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "issuer": { + "description": "The issuer of the Kubernetes service account tokens. Depending your Keycloak Realm \"ssl_required\" setting, this may need to be an HTTPS URL.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "Provider ID, is always kubernetes.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_custom_mapper": { + "block": { + "attributes": { + "config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "provider_id": { + "description": "ID of the custom LDAP mapper.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "provider_type": { + "description": "Fully-qualified name of the Java class implementing the custom LDAP mapper.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_full_name_mapper": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_full_name_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "read_only": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "write_only": { + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_group_mapper": { + "block": { + "attributes": { + "drop_non_existing_groups_during_sync": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "group_name_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "group_object_classes": { + "description_kind": "plain", + "required": true, + "type": [ + "list", + "string" + ] + }, + "groups_ldap_filter": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "groups_path": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ignore_missing_groups": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "ldap_groups_dn": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "mapped_group_attributes": { + "description_kind": "plain", + "optional": true, + "type": [ + "list", + "string" + ] + }, + "memberof_ldap_attribute": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "membership_attribute_type": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "membership_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "membership_user_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "mode": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "preserve_group_inheritance": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_roles_retrieve_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_hardcoded_attribute_mapper": { + "block": { + "attributes": { + "attribute_name": { + "description": "Name of the LDAP attribute", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "attribute_value": { + "description": "Value of the LDAP attribute. You can hardcode any value like 'foo'", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_hardcoded_group_mapper": { + "block": { + "attributes": { + "group": { + "description": "Group to grant to user.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_hardcoded_role_mapper": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role": { + "description": "Role to grant to user.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_msad_lds_user_account_control_mapper": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_msad_user_account_control_mapper": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_password_policy_hints_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_role_mapper": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ldap_roles_dn": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "memberof_ldap_attribute": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "membership_attribute_type": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "membership_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "membership_user_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "mode": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_name_ldap_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_object_classes": { + "description_kind": "plain", + "required": true, + "type": [ + "list", + "string" + ] + }, + "roles_ldap_filter": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "use_realm_roles_mapping": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "user_roles_retrieve_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_user_attribute_mapper": { + "block": { + "attributes": { + "always_read_value_from_ldap": { + "description": "When true, the value fetched from LDAP will override the value stored in Keycloak.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "attribute_default_value": { + "description": "Default value to set in LDAP if is_mandatory_in_ldap and the value is empty", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attribute_force_default": { + "description": "When true, an empty default value is forced for mandatory attributes even when a default value is not specified.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "is_binary_attribute": { + "description": "Should be true for binary LDAP attributes", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "is_mandatory_in_ldap": { + "description": "When true, this attribute must exist in LDAP.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "ldap_attribute": { + "description": "Name of the mapped attribute on LDAP object.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "ldap_user_federation_id": { + "description": "The ldap user federation provider to attach this mapper to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of the mapper when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "read_only": { + "description": "When true, this attribute is not saved back to LDAP when the user attribute is updated in Keycloak.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "realm_id": { + "description": "The realm in which the ldap user federation provider exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_model_attribute": { + "description": "Name of the UserModel property or attribute you want to map the LDAP attribute into.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_ldap_user_federation": { + "block": { + "attributes": { + "batch_size_for_sync": { + "description": "The number of users to sync within a single transaction.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "bind_credential": { + "description": "Password of LDAP admin.", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string" + }, + "bind_dn": { + "description": "DN of LDAP admin, which will be used by Keycloak to access LDAP server.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "changed_sync_period": { + "description": "How frequently Keycloak should sync changed LDAP users, in seconds. Omit this property to disable periodic changed users sync.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "connection_pooling": { + "description": "When true, Keycloak will use connection pooling when connecting to LDAP.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "connection_timeout": { + "description": "LDAP connection timeout (duration string)", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "connection_url": { + "description": "Connection URL to the LDAP server.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "custom_user_search_filter": { + "description": "Additional LDAP filter for filtering searched users. Must begin with '(' and end with ')'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "debug": { + "description": "true: enables debug logging for Krb5LoginModule. false: disables debug logging for Krb5LoginModule", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "delete_default_mappers": { + "description": "When true, the provider will delete the default mappers which are normally created by Keycloak when creating an LDAP user federation provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "edit_mode": { + "description": "READ_ONLY and WRITABLE are self-explanatory. UNSYNCED allows user data to be imported but not synced back to LDAP.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "When false, this provider will not be used when performing queries for users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "full_sync_period": { + "description": "How frequently Keycloak should sync all LDAP users, in seconds. Omit this property to disable periodic full sync.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "import_enabled": { + "description": "When true, LDAP users will be imported into the Keycloak database.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "krb_principal_attribute": { + "computed": true, + "description": "Name of the LDAP attribute, which refers to Kerberos principal. This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak. When this is empty, the LDAP user will be looked based on LDAP username corresponding to the first part of his Kerberos principal. For instance, for principal 'john@KEYCLOAK.ORG', it will assume that LDAP username is 'john'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of the provider when displayed in the console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "pagination": { + "description": "When true, Keycloak assumes the LDAP server supports pagination.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "priority": { + "description": "Priority of this provider when looking up users. Lower values are first.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "rdn_ldap_attribute": { + "description": "Name of the LDAP attribute to use as the relative distinguished name.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "read_timeout": { + "description": "LDAP read timeout (duration string)", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description": "The realm this provider will provide user federation for.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "search_scope": { + "description": "ONE_LEVEL: only search for users in the DN specified by user_dn. SUBTREE: search entire LDAP subtree.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "start_tls": { + "description": "When true, Keycloak will encrypt the connection to LDAP using STARTTLS, which will disable connection pooling.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_registrations": { + "description": "When true, newly created users will be synced back to LDAP.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "trust_email": { + "description": "If enabled, email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "use_password_modify_extended_op": { + "description": "When `true`, use the LDAPv3 Password Modify Extended Operation (RFC-3062).", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "use_truststore_spi": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "user_object_classes": { + "description": "All values of LDAP objectClass attribute for users in LDAP.", + "description_kind": "plain", + "required": true, + "type": [ + "list", + "string" + ] + }, + "username_ldap_attribute": { + "description": "Name of the LDAP attribute to use as the Keycloak username.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "users_dn": { + "description": "Full DN of LDAP tree where your users are.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "uuid_ldap_attribute": { + "description": "Name of the LDAP attribute to use as a unique object identifier for objects in LDAP.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "validate_password_policy": { + "description": "When true, Keycloak will validate passwords using the realm policy before updating it.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "vendor": { + "description": "LDAP vendor. I am almost certain this field does nothing, but the UI indicates that it is required.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "block_types": { + "cache": { + "block": { + "attributes": { + "eviction_day": { + "description": "Day of the week the entry will become invalid on.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "eviction_hour": { + "description": "Hour of day the entry will become invalid on.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "eviction_minute": { + "description": "Minute of day the entry will become invalid on.", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "max_lifespan": { + "description": "Max lifespan of cache entry (duration string).", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policy": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description": "Settings regarding cache policy for this realm.", + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "kerberos": { + "block": { + "attributes": { + "kerberos_realm": { + "description": "The name of the kerberos realm, e.g. FOO.LOCAL", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "key_tab": { + "description": "Path to the kerberos keytab file on the server with credentials of the service principal.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "server_principal": { + "description": "The kerberos server principal, e.g. 'HTTP/host.foo.com@FOO.LOCAL'.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "use_kerberos_for_password_authentication": { + "description": "Use kerberos login module instead of ldap service api. Defaults to `false`.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description": "Settings regarding kerberos authentication for this realm.", + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_oidc_facebook_identity_provider": { + "block": { + "attributes": { + "accepts_prompt_none_forward_from_client": { + "description": "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "computed": true, + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "The client identifier registered with the Facebook identity provider.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description": "The client secret registered with the Facebook identity provider.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "default_scopes": { + "description": "The scopes to be sent when asking for authorization. See the documentation for possible values, separator and default value'. Default: 'openid profile email'", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "disable_user_info": { + "description": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_name": { + "computed": true, + "description": "The human-friendly name of the identity provider, used in the log in form.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "fetched_fields": { + "description": "Provide additional fields which would be fetched using the profile request. This will be appended to the default set of 'id,name,email,first_name,last_name'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "provider id, is always facebook, unless you have a extended custom implementation", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_oidc_github_identity_provider": { + "block": { + "attributes": { + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "computed": true, + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri. In case of github this is computed and always github", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "api_url": { + "description": "API URL for the GitHub instance, defaults to https://api.github.com", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "base_url": { + "description": "Base URL for the GitHub instance, defaults to https://github.com", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "Client ID.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description": "Client Secret.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "default_scopes": { + "description": "The scopes to be sent when asking for authorization. See the documentation for possible values, separator and default value'. Default to 'user:email'", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_name": { + "computed": true, + "description": "The human-friendly name of the identity provider, used in the log in form.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "github_json_format": { + "description": "Whether GitHub API shoulds accept JSON explicitly during token authentication requests, defaults to false", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "provider id, is always github, unless you have a extended custom implementation", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_oidc_google_identity_provider": { + "block": { + "attributes": { + "accepts_prompt_none_forward_from_client": { + "description": "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "computed": true, + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri. In case of google this is computed and always google", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "Client ID.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description": "Client Secret.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "default_scopes": { + "description": "The scopes to be sent when asking for authorization. See the documentation for possible values, separator and default value'. Default: 'openid profile email'", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "disable_user_info": { + "description": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_name": { + "computed": true, + "description": "The human-friendly name of the identity provider, used in the log in form.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "hosted_domain": { + "description": "Set 'hd' query parameter when logging in with Google. Google will list accounts only for this domain. Keycloak validates that the returned identity token has a claim for this domain. When '*' is entered, any hosted account can be used.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "login_hint": { + "description": "Pass login_hint to identity provider. Set to \"true\" to forward the login_hint client note (the underlying loginHint config attribute is a boolean string).", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "provider id, is always google, unless you have a extended custom implementation", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "request_refresh_token": { + "description": "Set 'access_type' query parameter to 'offline' when redirecting to google authorization endpoint, to get a refresh token back. Useful if planning to use Token Exchange to retrieve Google token to access Google APIs when the user is not at the browser.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "use_user_ip_param": { + "description": "Set 'userIp' query parameter when invoking on Google's User Info service. This will use the user's ip address. Useful if Google is throttling access to the User Info service.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_oidc_identity_provider": { + "block": { + "attributes": { + "accepts_prompt_none_forward_from_client": { + "description": "This is just used together with Identity Provider Authenticator or when kc_idp_hint points to this identity provider. In case that client sends a request with prompt=none and user is not yet authenticated, the error will not be directly returned to client, but the request with prompt=none will be forwarded to this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "authorization_url": { + "description": "OIDC authorization URL.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "backchannel_supported": { + "description": "Does the external IDP support backchannel logout?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "Client ID.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description": "Client Secret.", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string" + }, + "client_secret_wo": { + "description": "Client Secret as write-only argument", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string", + "write_only": true + }, + "client_secret_wo_version": { + "description": "Version of the Client secret write-only argument", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "default_scopes": { + "description": "The scopes to be sent when asking for authorization. It can be a space-separated list of scopes. Defaults to 'openid'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "disable_type_claim_check": { + "description": "Disables the validation of the `typ` claim of tokens received from the Identity Provider. If this is `off` the type claim is validated (default).", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "disable_user_info": { + "description": "Disable usage of User Info service to obtain additional user information? Default is to use this OIDC service.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_name": { + "computed": true, + "description": "The human-friendly name of the identity provider, used in the log in form.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "issuer": { + "description": "The issuer identifier for the issuer of the response. If not provided, no validation will be performed.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "jwks_url": { + "description": "JSON Web Key Set URL", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "login_hint": { + "description": "Login Hint.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logout_url": { + "description": "Logout URL", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "provider id, is always oidc, unless you have a custom implementation", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "token_url": { + "description": "Token URL.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "ui_locales": { + "description": "Pass current locale to identity provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "user_info_url": { + "description": "User Info URL", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "validate_signature": { + "description": "Enable/disable signature validation of external IDP signatures.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_oidc_openshift_v4_identity_provider": { + "block": { + "attributes": { + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "computed": true, + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri. Defaults to openshift-v4 if not set.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "base_url": { + "description": "Base URL of the OpenShift 4 cluster, e.g. https://openshift.example.com:8443.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_id": { + "description": "Client ID.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description": "Client Secret.", + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "default_scopes": { + "description": "The scopes to be sent when asking for authorization. Defaults to 'user:full'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_name": { + "computed": true, + "description": "The human-friendly name of the identity provider, used in the log in form.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "Provider ID; always openshift-v4 unless you have an extended custom implementation.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_audience_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if this claim should be added to the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if this claim should be added to the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "included_client_audience": { + "description": "A client ID to include within the token's `aud` claim. Cannot be used with included_custom_audience", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "included_custom_audience": { + "description": "A custom audience to include within the token's `aud` claim. Cannot be used with included_custom_audience", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_audience_resolve_protocol_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client": { + "block": { + "attributes": { + "access_token_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "access_type": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "admin_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "allow_refresh_token_in_standard_token_exchange": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "always_display_in_console": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "backchannel_logout_revoke_offline_sessions": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "backchannel_logout_session_required": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "backchannel_logout_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "base_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_authenticator_type": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_offline_session_idle_timeout": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_offline_session_max_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_secret": { + "computed": true, + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string" + }, + "client_secret_regenerate_when_changed": { + "description": "Arbitrary map of values that, when changed, will trigger rotation of the secret", + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "client_secret_wo": { + "description": "Client Secret as write-only argument", + "description_kind": "plain", + "optional": true, + "sensitive": true, + "type": "string", + "write_only": true + }, + "client_secret_wo_version": { + "description": "Version of the Client secret write-only argument", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "client_session_idle_timeout": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_session_max_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "consent_required": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "consent_screen_text": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "direct_access_grants_enabled": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_on_consent_screen": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "exclude_issuer_from_auth_response": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "exclude_session_state_from_auth_response": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "frontchannel_logout_enabled": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "frontchannel_logout_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "full_scope_allowed": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "implicit_flow_enabled": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "import": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "login_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "oauth2_device_authorization_grant_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "oauth2_device_code_lifespan": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "oauth2_device_polling_interval": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "oauth2_jwt_authorization_grant_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "oauth2_jwt_authorization_grant_idp": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "pkce_code_challenge_method": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "require_dpop_bound_tokens": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "resource_server_id": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "root_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "service_account_user_id": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "service_accounts_enabled": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "standard_flow_enabled": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "standard_token_exchange_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "use_refresh_tokens": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "use_refresh_tokens_client_credentials": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "valid_post_logout_redirect_uris": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "valid_redirect_uris": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "web_origins": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "block_types": { + "authentication_flow_binding_overrides": { + "block": { + "attributes": { + "browser_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "direct_grant_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "authorization": { + "block": { + "attributes": { + "allow_remote_resource_management": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "keep_defaults": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "policy_enforcement_mode": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_aggregate_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_authorization_client_scope_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "scope": { + "block": { + "attributes": { + "id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "required": { + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "min_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_authorization_permission": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_type": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "resources": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "scopes": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "type": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_authorization_resource": { + "block": { + "attributes": { + "attributes": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "icon_uri": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "owner_managed_access": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "scopes": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "type": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "uris": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_authorization_scope": { + "block": { + "attributes": { + "display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "icon_uri": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_client_policy": { + "block": { + "attributes": { + "clients": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_default_scopes": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "default_scopes": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_group_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "groups_claim": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "groups": { + "block": { + "attributes": { + "extend_children": { + "description_kind": "plain", + "required": true, + "type": "bool" + }, + "id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "path": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "min_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_optional_scopes": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "optional_scopes": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_permissions": { + "block": { + "attributes": { + "authorization_resource_server_id": { + "computed": true, + "description": "Resource server id representing the realm management client on which this permission is managed", + "description_kind": "plain", + "type": "string" + }, + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "enabled": { + "computed": true, + "description_kind": "plain", + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "configure_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "manage_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "map_roles_client_scope_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "map_roles_composite_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "map_roles_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "token_exchange_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "view_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_regex_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "pattern": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "target_claim": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "target_context_attributes": { + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_role_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "fetch_roles": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "type": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "role": { + "block": { + "attributes": { + "id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "required": { + "description_kind": "plain", + "required": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "min_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_scope": { + "block": { + "attributes": { + "consent_screen_text": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "gui_order": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "include_in_token_scope": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_service_account_realm_role": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "service_account_user_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_service_account_role": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "service_account_user_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_time_policy": { + "block": { + "attributes": { + "day_month": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "day_month_end": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "decision_strategy": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hour": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hour_end": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "minute": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "minute_end": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "month": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "month_end": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "not_before": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "not_on_or_after": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "year": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "year_end": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_client_user_policy": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logic": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "resource_server_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "users": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_full_name_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_group_membership_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "full_path": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_hardcoded_claim_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the attribute should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the attribute should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_hardcoded_role_protocol_mapper": { + "block": { + "attributes": { + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_sub_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_token_introspection": { + "description": "Indicates if the attribute should be a claim in the token introspection response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_user_attribute_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the attribute should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_token_introspection": { + "description": "Indicates if the attribute should be a claim in the token introspection response.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the attribute should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "aggregate_attributes": { + "description": "Indicates if attribute values should be aggregated within the group attributes", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "multivalued": { + "description": "Indicates whether this attribute is a single value or an array of values.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_user_client_role_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the attribute should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the attribute should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id_for_role_mappings": { + "description": "Client ID for role mappings.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_role_prefix": { + "description": "Prefix that will be added to each client role.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "multivalued": { + "description": "Indicates whether this attribute is a single value or an array of values.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_user_property_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the property should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the property should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the property should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_property": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_user_realm_role_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the attribute should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_token_introspection": { + "description": "Indicates if the attribute should be a claim in the token introspection response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the attribute should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "multivalued": { + "description": "Indicates whether this attribute is a single value or an array of values.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_role_prefix": { + "description": "Prefix that will be added to each realm role.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_openid_user_session_note_protocol_mapper": { + "block": { + "attributes": { + "add_to_access_token": { + "description": "Indicates if the attribute should be a claim in the access token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_id_token": { + "description": "Indicates if the attribute should be a claim in the id token.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_token_introspection": { + "description": "Indicates if the session note should be a claim in the token introspection response.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "add_to_userinfo": { + "description": "Indicates if the session note should appear in the userinfo response body.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "claim_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "claim_value_type": { + "description": "Claim type used when serializing tokens.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description": "The mapper's associated client. Cannot be used at the same time as client_scope_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description": "The mapper's associated client scope. Cannot be used at the same time as client_id.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "A human-friendly name that will appear in the Keycloak console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm id where the associated client or client scope exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "session_note": { + "description": "String value being the name of stored user session note within the UserSessionModel.note map.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_organization": { + "block": { + "attributes": { + "alias": { + "computed": true, + "description": "The alias unique identifies the organization. Same as the name if not specified.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attributes": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this organization.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "The name of the organization.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm ID.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "redirect_url": { + "description": "Landing page after successful login.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "block_types": { + "domain": { + "block": { + "attributes": { + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "verified": { + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm": { + "block": { + "attributes": { + "access_code_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "access_code_lifespan_login": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "access_code_lifespan_user_action": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "access_token_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "access_token_lifespan_for_implicit_flow": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "account_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "action_token_generated_by_admin_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "action_token_generated_by_user_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "admin_permissions_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "admin_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "attributes": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "browser_flow": { + "computed": true, + "description": "Which flow should be used for BrowserFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_authentication_flow": { + "computed": true, + "description": "Which flow should be used for ClientAuthenticationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_session_idle_timeout": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_session_max_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "default_default_client_scopes": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "default_optional_client_scopes": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "default_signature_algorithm": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "direct_grant_flow": { + "computed": true, + "description": "Which flow should be used for DirectGrantFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_name_html": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "docker_authentication_flow": { + "computed": true, + "description": "Which flow should be used for DockerAuthenticationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "duplicate_emails_allowed": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "edit_username_allowed": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "email_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "first_broker_login_flow": { + "computed": true, + "description": "Which flow should be used for FirstBrokerLoginFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "login_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "login_with_email_allowed": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "oauth2_device_code_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "oauth2_device_polling_interval": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "offline_session_idle_timeout": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "offline_session_max_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "offline_session_max_lifespan_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organizations_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "password_policy": { + "description": "String that represents the passwordPolicies that are in place. Each policy is separated with \" and \". Supported policies can be found in the server-info providers page. example: \"upperCase(1) and length(8) and forceExpiredPasswordChange(365) and notUsername(undefined)\"", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "refresh_token_max_reuse": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "registration_allowed": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "registration_email_as_username": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "registration_flow": { + "computed": true, + "description": "Which flow should be used for RegistrationFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "remember_me": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "reset_credentials_flow": { + "computed": true, + "description": "Which flow should be used for ResetCredentialsFlow", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "reset_password_allowed": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "revoke_refresh_token": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "ssl_required": { + "description": "SSL Required: Values can be 'none', 'external' or 'all'.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "sso_session_idle_timeout": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "sso_session_idle_timeout_remember_me": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "sso_session_max_lifespan": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "sso_session_max_lifespan_remember_me": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "terraform_deletion_protection": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "user_managed_access": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "verify_email": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "block_types": { + "internationalization": { + "block": { + "attributes": { + "default_locale": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "supported_locales": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "otp_policy": { + "block": { + "attributes": { + "algorithm": { + "description": "What hashing algorithm should be used to generate the OTP.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "code_reusable": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "digits": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "initial_counter": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "look_ahead_window": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "period": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "type": { + "description": "OTP Type, totp for Time-Based One Time Password or hotp for counter base one time password", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "security_defenses": { + "block": { + "block_types": { + "brute_force_detection": { + "block": { + "attributes": { + "brute_force_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "failure_reset_time_seconds": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "max_failure_wait_seconds": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "max_login_failures": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "max_temporary_lockouts": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "minimum_quick_login_wait_seconds": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "permanent_lockout": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "quick_login_check_milli_seconds": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "wait_increment_seconds": { + "description_kind": "plain", + "optional": true, + "type": "number" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "headers": { + "block": { + "attributes": { + "content_security_policy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "content_security_policy_report_only": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "referrer_policy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "strict_transport_security": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "x_content_type_options": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "x_frame_options": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "x_robots_tag": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "x_xss_protection": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "smtp_server": { + "block": { + "attributes": { + "allow_utf8": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "envelope_from": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "from": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "from_display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "host": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "port": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "reply_to": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "reply_to_display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "ssl": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "starttls": { + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "block_types": { + "auth": { + "block": { + "attributes": { + "password": { + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "username": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "token_auth": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_secret": { + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + }, + "scope": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "url": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "username": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "web_authn_passwordless_policy": { + "block": { + "attributes": { + "acceptable_aaguids": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "attestation_conveyance_preference": { + "description": "Either none, indirect or direct", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticator_attachment": { + "description": "Either platform or cross-platform", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "avoid_same_authenticator_register": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "create_timeout": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "extra_origins": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "passwordless_passkeys_enabled": { + "description": "Enable passkeys for passwordless WebAuthn authentication", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "relying_party_entity_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "relying_party_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "require_resident_key": { + "description": "Either Yes or No", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signature_algorithms": { + "computed": true, + "description": "Keycloak lists ES256, ES384, ES512, RS256, RS384, RS512, RS1 at the time of writing", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "user_verification_requirement": { + "description": "Either required, preferred or discouraged", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "web_authn_policy": { + "block": { + "attributes": { + "acceptable_aaguids": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "attestation_conveyance_preference": { + "description": "Either none, indirect or direct", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authenticator_attachment": { + "description": "Either platform or cross-platform", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "avoid_same_authenticator_register": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "create_timeout": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "extra_origins": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "relying_party_entity_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "relying_party_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "require_resident_key": { + "description": "Either Yes or No", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signature_algorithms": { + "computed": true, + "description": "Keycloak lists ES256, ES384, ES512, RS256, RS384, RS512, RS1 at the time of writing", + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "user_verification_requirement": { + "description": "Either required, preferred or discouraged", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_client_policy_profile": { + "block": { + "attributes": { + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "executor": { + "block": { + "attributes": { + "configuration": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_client_policy_profile_policy": { + "block": { + "attributes": { + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "profiles": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "condition": { + "block": { + "attributes": { + "configuration": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_default_client_scopes": { + "block": { + "attributes": { + "default_scopes": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_events": { + "block": { + "attributes": { + "admin_events_details_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "admin_events_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enabled_event_types": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "events_enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "events_expiration": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "events_listeners": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_aes_generated": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "secret_size": { + "description": "Size in bytes for the generated AES Key. Size 16 is for AES-128, Size 24 for AES-192 and Size 32 for AES-256. WARN: Bigger keys then 128 bits are not allowed on some JDK implementations", + "description_kind": "plain", + "optional": true, + "type": "number" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_ecdsa_generated": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "elliptic_curve_key": { + "description": "Elliptic Curve used in ECDSA", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_hmac_generated": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "algorithm": { + "description": "Intended algorithm for the key", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "secret_size": { + "description": "Size in bytes for the generated secret", + "description_kind": "plain", + "optional": true, + "type": "number" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_java_keystore": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "algorithm": { + "description": "Intended algorithm for the key", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "key_alias": { + "description": "Alias for the private key", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "key_password": { + "description": "Password for the private key", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "key_use": { + "description": "Intended use for the key", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "keystore": { + "description": "Path to keys file", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "keystore_password": { + "description": "Password for the keys", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "parent_id": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_rsa": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "algorithm": { + "description": "Intended algorithm for the key", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "certificate": { + "description": "X509 Certificate encoded in PEM format", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "private_key": { + "description": "Private RSA Key encoded in PEM format", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "provider_id": { + "description": "RSA key provider id", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_keystore_rsa_generated": { + "block": { + "attributes": { + "active": { + "description": "Set if the keys can be used for signing", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "algorithm": { + "description": "Intended algorithm for the key", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Set if the keys are enabled", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "key_size": { + "description": "Size for the generated keys", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "name": { + "description": "Display name of provider when linked in admin console.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "priority": { + "description": "Priority for the provider", + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_localization": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "locale": { + "description": "The locale for the localization texts.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description": "The realm in which the texts exists.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "texts": { + "description": "The mapping of localization texts keys to values.", + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + } + }, + "description": "Manage realm-level localization texts.", + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_optional_client_scopes": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "optional_scopes": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_realm_user_profile": { + "block": { + "attributes": { + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "unmanaged_attribute_policy": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "block_types": { + "attribute": { + "block": { + "attributes": { + "annotations": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "default_value": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled_when_scope": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "group": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "multi_valued": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "required_for_roles": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "required_for_scopes": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "block_types": { + "permissions": { + "block": { + "attributes": { + "edit": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "view": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + }, + "validator": { + "block": { + "attributes": { + "config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "list" + }, + "group": { + "block": { + "attributes": { + "annotations": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "display_description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "display_header": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_required_action": { + "block": { + "attributes": { + "alias": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "default_action": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "priority": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_role": { + "block": { + "attributes": { + "attributes": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "client_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "composite_roles": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "description": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "import": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_client": { + "block": { + "attributes": { + "always_display_in_console": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "assertion_consumer_post_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "assertion_consumer_redirect_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "base_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "canonicalization_method": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "client_signature_required": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "consent_required": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "encrypt_assertions": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "encryption_algorithm": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "encryption_certificate": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "encryption_certificate_sha1": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "encryption_digest_method": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "encryption_key_algorithm": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "encryption_mask_generation_function": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "force_name_id_format": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "force_post_binding": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "front_channel_logout": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "full_scope_allowed": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "idp_initiated_sso_relay_state": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "idp_initiated_sso_url_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "include_authn_statement": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "login_theme": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logout_service_post_binding_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "logout_service_redirect_binding_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "master_saml_processing_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name_id_format": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "root_url": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "sign_assertions": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sign_documents": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "signature_algorithm": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signature_key_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signing_certificate": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signing_certificate_sha1": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "signing_private_key": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signing_private_key_sha1": { + "computed": true, + "description_kind": "plain", + "type": "string" + }, + "valid_redirect_uris": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "block_types": { + "authentication_flow_binding_overrides": { + "block": { + "attributes": { + "browser_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "direct_grant_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_client_default_scopes": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "default_scopes": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_client_scope": { + "block": { + "attributes": { + "consent_screen_text": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "gui_order": { + "description_kind": "plain", + "optional": true, + "type": "number" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_identity_provider": { + "block": { + "attributes": { + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "authn_context_class_refs": { + "description": "AuthnContext ClassRefs", + "description_kind": "plain", + "optional": true, + "type": [ + "list", + "string" + ] + }, + "authn_context_comparison_type": { + "description": "AuthnContext Comparison", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "authn_context_decl_refs": { + "description": "AuthnContext DeclRefs", + "description_kind": "plain", + "optional": true, + "type": [ + "list", + "string" + ] + }, + "backchannel_supported": { + "description": "Does the external IDP support backchannel logout?", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "display_name": { + "description": "Friendly name for Identity Providers.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "entity_id": { + "description": "The Entity ID that will be used to uniquely identify this SAML Service Provider.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "force_authn": { + "description": "Require Force Authn.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "description": "Hide On Login Page.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "login_hint": { + "description": "Login Hint.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name_id_policy_format": { + "description": "Name ID Policy Format.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_binding_authn_request": { + "description": "Post Binding Authn Request.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "post_binding_logout": { + "description": "Post Binding Logout.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "post_binding_response": { + "description": "Post Binding Response.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "principal_attribute": { + "description": "Principal Attribute", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "principal_type": { + "description": "Principal Type", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "provider id, is always saml, unless you have a custom implementation", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "signature_algorithm": { + "description": "Signing Algorithm.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "signing_certificate": { + "description": "Signing Certificate.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "single_logout_service_url": { + "description": "Logout URL.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "single_sign_on_service_url": { + "description": "SSO Logout URL.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "validate_signature": { + "description": "Enable/disable signature validation of SAML responses.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "want_assertions_encrypted": { + "description": "Want Assertions Encrypted.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "want_assertions_signed": { + "description": "Want Assertions Signed.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "want_authn_requests_signed": { + "computed": true, + "description": "Want Authn Requests Signed.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "xml_sign_key_info_key_name_transformer": { + "description": "Sign Key Transformer.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_user_attribute_protocol_mapper": { + "block": { + "attributes": { + "aggregate_attributes": { + "description": "Indicates if attribute values should be aggregated within the group attributes", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "client_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "friendly_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "saml_attribute_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "saml_attribute_name_format": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_attribute": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_saml_user_property_protocol_mapper": { + "block": { + "attributes": { + "client_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "client_scope_id": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "friendly_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "saml_attribute_name": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "saml_attribute_name_format": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_property": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_spiffe_identity_provider": { + "block": { + "attributes": { + "add_read_token_role_on_create": { + "description": "Enable/disable if new users can read any stored tokens. This assigns the broker.read-token role.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "alias": { + "description": "The alias uniquely identifies an identity provider and it is also used to build the redirect uri.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "authenticate_by_default": { + "description": "Enable/disable authenticate users by default.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "bundle_endpoint": { + "description": "The SPIFFE bundle endpoint or OpenID Connect JWKS endpoint exposing SPIFFE public keys. Depending on your Keycloak Realm \"ssl_required\" setting, this may need to be an HTTPS URL.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "display_name": { + "description": "Friendly name for Identity Providers.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Enable/disable this identity provider.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "first_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that there is not yet existing Keycloak account linked with the authenticated identity provider account.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "gui_order": { + "description": "GUI Order", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "hide_on_login_page": { + "computed": true, + "description": "This is always set to true for SPIFFE identity provider.", + "description_kind": "plain", + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "internal_id": { + "computed": true, + "description": "Internal Identity Provider Id", + "description_kind": "plain", + "type": "string" + }, + "link_only": { + "description": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "org_domain": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "org_redirect_mode_email_matches": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "organization_id": { + "description": "ID of organization with which this identity is linked.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "post_broker_login_flow_alias": { + "description": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this empty if you don't want any additional authenticators to be triggered after login with this identity provider. Also note, that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "provider_id": { + "description": "Provider ID, is always spiffe.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "store_token": { + "description": "Enable/disable if tokens must be stored after authenticating users.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "sync_mode": { + "description": "Sync Mode", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "trust_domain": { + "description": "The SPIFFE trust domain. This must use the spiffe:// scheme.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "trust_email": { + "description": "If enabled then email provided by this provider is not verified even if verification is enabled for the realm.", + "description_kind": "plain", + "optional": true, + "type": "bool" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_user": { + "block": { + "attributes": { + "attributes": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "email": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "email_verified": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "enabled": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "first_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "import": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "last_name": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "required_actions": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + }, + "username": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "federated_identity": { + "block": { + "attributes": { + "identity_provider": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_name": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "nesting_mode": "set" + }, + "initial_password": { + "block": { + "attributes": { + "temporary": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "value": { + "description_kind": "plain", + "required": true, + "sensitive": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_user_groups": { + "block": { + "attributes": { + "exhaustive": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "group_ids": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "user_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_user_roles": { + "block": { + "attributes": { + "exhaustive": { + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + }, + "role_ids": { + "description_kind": "plain", + "required": true, + "type": [ + "set", + "string" + ] + }, + "user_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_user_template_importer_identity_provider_mapper": { + "block": { + "attributes": { + "extra_config": { + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "identity_provider_alias": { + "description": "IDP Alias", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "name": { + "description": "IDP Mapper Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "Realm Name", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "template": { + "description": "Username For Template Import", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_users_permissions": { + "block": { + "attributes": { + "authorization_resource_server_id": { + "computed": true, + "description": "Resource server id representing the realm management client on which this permission is managed", + "description_kind": "plain", + "type": "string" + }, + "enabled": { + "computed": true, + "description_kind": "plain", + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "realm_id": { + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "block_types": { + "impersonate_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "manage_group_membership_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "manage_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "map_roles_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "user_impersonated_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + }, + "view_scope": { + "block": { + "attributes": { + "decision_strategy": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "description": { + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "policies": { + "description_kind": "plain", + "optional": true, + "type": [ + "set", + "string" + ] + } + }, + "description_kind": "plain" + }, + "max_items": 1, + "nesting_mode": "set" + } + }, + "description_kind": "plain" + }, + "version": 0 + }, + "keycloak_workflow": { + "block": { + "attributes": { + "cancel_in_progress": { + "description": "Event that cancels an in-progress workflow execution.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "conditions": { + "description": "Expression that must be satisfied for the workflow to run.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "enabled": { + "description": "Whether the workflow is enabled.", + "description_kind": "plain", + "optional": true, + "type": "bool" + }, + "id": { + "computed": true, + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "name": { + "description": "The name of the workflow.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "on": { + "description": "The event that triggers the workflow. Supported values: user_created, user_removed, user_authenticated, user_federated_identity_added, user_federated_identity_removed, user_group_membership_added, user_group_membership_removed, user_role_granted, user_role_revoked.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "realm": { + "description": "The realm this workflow belongs to.", + "description_kind": "plain", + "required": true, + "type": "string" + }, + "restart_in_progress": { + "description": "Event that restarts an in-progress workflow execution.", + "description_kind": "plain", + "optional": true, + "type": "string" + } + }, + "block_types": { + "step": { + "block": { + "attributes": { + "after": { + "description": "Delay in milliseconds before executing this step.", + "description_kind": "plain", + "optional": true, + "type": "string" + }, + "config": { + "description": "Key-value configuration for the step.", + "description_kind": "plain", + "optional": true, + "type": [ + "map", + "string" + ] + }, + "uses": { + "description": "The step type to execute (e.g. disable-user, delete-user, notify-user).", + "description_kind": "plain", + "required": true, + "type": "string" + } + }, + "description": "Ordered list of steps to execute.", + "description_kind": "plain" + }, + "min_items": 1, + "nesting_mode": "list" + } + }, + "description_kind": "plain" + }, + "version": 0 + } + }, + "source": "keycloak/keycloak", + "version": "5.8.0" +} diff --git a/services/keycloak/schema.nix b/services/keycloak/schema.nix new file mode 100644 index 0000000..b334772 --- /dev/null +++ b/services/keycloak/schema.nix @@ -0,0 +1,6 @@ +# The vendored keycloak/keycloak provider schema, parsed. +# +# Indirection, not decoration: Nix memoizes `import ` but not +# `builtins.readFile`, and `lib.nix` is instantiated once per system per check. +# Refresh with `nix run .#update-provider-schemas`. +builtins.fromJSON (builtins.readFile ./provider-schema.json) From ddf3e3249fe54aaa084b0f2caa32eaa7e7e709d0 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:43:22 +0200 Subject: [PATCH 12/18] fix(services/keycloak): drop three resources absent from provider 5.8.0 `keycloak_openid_client_js_policy`, `keycloak_openid_script_protocol_mapper` and `keycloak_saml_script_protocol_mapper` were removed from the provider along with Keycloak's `upload-scripts` feature, but the pairing kept offering options for them. Declaring any of the three produced a `.tf.json` naming a resource type the provider no longer implements, so the reconciler failed at apply time on a live host -- the exact class of drift the vendored schema now makes an eval-time error. Nothing but the removal is intended here: it keeps the schema-derived refactor a pure refactor rather than mixing a behaviour fix into it. Assisted-by: Claude:claude-opus-5 --- services/keycloak/README.md | 3 -- services/keycloak/lib.nix | 85 ------------------------------------- 2 files changed, 88 deletions(-) diff --git a/services/keycloak/README.md b/services/keycloak/README.md index efa3d1f..d4815bf 100644 --- a/services/keycloak/README.md +++ b/services/keycloak/README.md @@ -288,7 +288,6 @@ optional `client_scope` → openid_client_scopes): | `openid_user_realm_role_protocol_mappers` | `openid_user_realm_role_protocol_mapper` | | `openid_user_client_role_protocol_mappers` | `openid_user_client_role_protocol_mapper` | | `openid_user_session_note_protocol_mappers` | `openid_user_session_note_protocol_mapper` | -| `openid_script_protocol_mappers` | `openid_script_protocol_mapper` | SAML mappers (`realm` + optional `client` → saml_clients + optional `client_scope` → saml_client_scopes): @@ -297,7 +296,6 @@ SAML mappers (`realm` + optional `client` → saml_clients + optional | -------------------------------------- | ------------------------------------- | | `saml_user_attribute_protocol_mappers` | `saml_user_attribute_protocol_mapper` | | `saml_user_property_protocol_mappers` | `saml_user_property_protocol_mapper` | -| `saml_script_protocol_mappers` | `saml_script_protocol_mapper` | Generic mappers (`realm` + optional `client` → openid/saml clients + optional `client_scope` → openid/saml client_scopes, multi-target): @@ -354,7 +352,6 @@ collection, multi-target with literal fallback): | `openid_client_client_policies` | `openid_client_client_policy` | `name` | | `openid_client_authorization_client_scope_policies` | `openid_client_authorization_client_scope_policy` | `name` | | `openid_client_group_policies` | `openid_client_group_policy` | `name` | -| `openid_client_js_policies` | `openid_client_js_policy` | `name` | | `openid_client_role_policies` | `openid_client_role_policy` | `name` | | `openid_client_time_policies` | `openid_client_time_policy` | `name` | | `openid_client_user_policies` | `openid_client_user_policy` | `name` | diff --git a/services/keycloak/lib.nix b/services/keycloak/lib.nix index 0240269..3c54f6b 100644 --- a/services/keycloak/lib.nix +++ b/services/keycloak/lib.nix @@ -1333,30 +1333,6 @@ let }; }; - openid_script_protocol_mappers = { - type = "keycloak_openid_script_protocol_mapper"; - prefix = "openid_script_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "script" - "claim_name" - ]; - description = "OpenID protocol mapper that produces a claim from a JavaScript expression (requires the scripts feature)."; - attrs = openidMapperCommonAttrs // { - multivalued = oBool "Treat as multivalued?"; - script = oStr "JavaScript expression evaluated to produce the claim value."; - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type."; - }; - }; - saml_user_attribute_protocol_mappers = { type = "keycloak_saml_user_attribute_protocol_mapper"; prefix = "saml_user_attribute_mapper"; @@ -1408,32 +1384,6 @@ let }; }; - saml_script_protocol_mappers = { - type = "keycloak_saml_script_protocol_mapper"; - prefix = "saml_script_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = samlClientOptionalRef; - client_scope = samlClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "script" - "saml_attribute_name" - ]; - description = "SAML mapper that produces a SAML attribute from a JavaScript expression."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - single_value_attribute = oBool "Emit as a single-value attribute?"; - script = oStr "JavaScript expression evaluated to produce the SAML attribute value."; - friendly_name = oStr "Optional SAML friendlyName."; - saml_attribute_name = oStr "SAML attribute name."; - saml_attribute_name_format = oStr "SAML attribute name format."; - }; - }; - generic_protocol_mappers = { type = "keycloak_generic_protocol_mapper"; prefix = "generic_protocol_mapper"; @@ -2157,41 +2107,6 @@ let }; }; - openid_client_js_policies = { - type = "keycloak_openid_client_js_policy"; - prefix = "openid_client_js_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "code" - ]; - description = "Policy implemented in JavaScript (requires the scripts feature)."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - type = oStr "Policy type discriminator ('js')."; - code = oStr "JavaScript source."; - }; - }; - openid_client_role_policies = { type = "keycloak_openid_client_role_policy"; prefix = "openid_client_role_policy"; From c327056651570373d87148359dc416972153f5c7 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 17:49:15 +0200 Subject: [PATCH 13/18] refactor(services/keycloak): extract test fixtures into fixtures.nix The six `services.keycloak.runtime` blocks the VM tests declare move to `fixtures.nix`, and `keycloak-rendered-fixtures` renders them through the real option system and renderer into a `.tf.json` snapshot. That snapshot is the acceptance evidence for the coming schema-derived resource surface: build it before and after, and an empty diff proves the wire format is untouched. Pure refactor -- all six test derivations resolve to the same store path. Assisted-by: Claude:claude-opus-5 --- flake.nix | 10 + services/keycloak/checks.nix | 355 ++------------------------------ services/keycloak/fixtures.nix | 359 +++++++++++++++++++++++++++++++++ 3 files changed, 383 insertions(+), 341 deletions(-) create mode 100644 services/keycloak/fixtures.nix diff --git a/flake.nix b/flake.nix index 877a8ec..50030a0 100644 --- a/flake.nix +++ b/flake.nix @@ -95,6 +95,16 @@ tfConfig = libs.forgejo.forgejoTfConfig; fixtures = import ./services/forgejo/fixtures.nix; }; + + keycloak-rendered-fixtures = renderFixtures { + name = "keycloak"; + options = libs.keycloak.resourceOptions // { + baseUrl = urlOption "http://localhost:8080"; + adminRealm = urlOption "master"; + }; + tfConfig = libs.keycloak.keycloakTfConfig; + fixtures = import ./services/keycloak/fixtures.nix; + }; }; # `-schema-coverage`: the pairing's coverage table. Building it forces diff --git a/services/keycloak/checks.nix b/services/keycloak/checks.nix index b5630c0..25cdc8e 100644 --- a/services/keycloak/checks.nix +++ b/services/keycloak/checks.nix @@ -5,6 +5,10 @@ let inherit (pkgs) lib; keycloakAdminPassword = "hackme"; + # the `services.keycloak.runtime` blocks these tests converge, shared with + # the `keycloak-rendered-fixtures` snapshot. + fixtures = import ./fixtures.nix; + # python helpers; each takes the machine reference (`machine` in the VM # test, `keycloak` in the container tests) so bodies match. pyHelpers = '' @@ -88,20 +92,11 @@ in nodes.machine = args: - lib.recursiveUpdate - (mkHost { - runtime.realms.acme = { - display_name = "ACME Corp."; - display_name_html = "ACME Corp."; - }; - } args) - { - # keycloak is thicc; only VMs accept memorySize. - virtualisation.memorySize = 3072; - specialisation.addRealm.configuration.services.keycloak.runtime.realms.delta = { - display_name = "Delta Realm"; - }; - }; + lib.recursiveUpdate (mkHost { runtime = fixtures.core; } args) { + # keycloak is thicc; only VMs accept memorySize. + virtualisation.memorySize = 3072; + specialisation.addRealm.configuration.services.keycloak.runtime = fixtures.coreAddRealm; + }; testScript = '' ${pyHelpers} @@ -152,64 +147,7 @@ in keycloak-rbac = pkgs.testers.runNixOSTest { name = "declarative-keycloak-rbac"; - containers.keycloak = mkHost { - runtime = { - realms.acme.display_name = "ACME"; - - roles.acme_engineer = { - realm = "acme"; - name = "engineer"; - description = "ACME engineering role"; - }; - default_roles.acme = { - realm = "acme"; - default_roles = [ - "offline_access" - "uma_authorization" - "acme_engineer" # managed key, resolves to role name "engineer" - ]; - }; - - groups.acme_eng = { - realm = "acme"; - name = "engineering"; - attributes."team" = "infra"; - }; - groups.acme_eng_backend = { - realm = "acme"; - name = "backend"; - parent = "acme_eng"; - }; - group_roles.acme_eng_admins = { - realm = "acme"; - group = "acme_eng"; - role_ids = [ "acme_engineer" ]; # managed key - exhaustive = true; - }; - - users.acme_alice = { - realm = "acme"; - username = "alice"; - email = "alice@acme.example"; - first_name = "Alice"; - last_name = "Anderson"; - email_verified = true; - required_actions = [ "UPDATE_PASSWORD" ]; - }; - user_roles.acme_alice = { - realm = "acme"; - user = "acme_alice"; - role_ids = [ "acme_engineer" ]; - exhaustive = false; - }; - user_groups.acme_alice = { - realm = "acme"; - user = "acme_alice"; - group_ids = [ "acme_eng" ]; # managed key - exhaustive = false; - }; - }; - }; + containers.keycloak = mkHost { runtime = fixtures.rbac; }; testScript = '' ${pyHelpers} @@ -259,55 +197,7 @@ in containers.keycloak = mkHost { extraEtc."acme-app-client-secret".text = "topsecret"; - runtime = { - realms.acme.display_name = "ACME"; - - openid_client_scopes.acme_profile = { - realm = "acme"; - name = "acme-profile"; - description = "ACME profile scope"; - consent_screen_text = "Access your ACME profile"; - include_in_token_scope = true; - gui_order = 10; - }; - - openid_clients.acme_app = { - realm = "acme"; - client_id = "acme-app"; - name = "ACME App"; - access_type = "CONFIDENTIAL"; - client_secretFile = "/etc/acme-app-client-secret"; - standard_flow_enabled = true; - direct_access_grants_enabled = true; - service_accounts_enabled = true; - valid_redirect_uris = [ "https://app.acme.example/*" ]; - web_origins = [ "https://app.acme.example" ]; - consent_required = false; - full_scope_allowed = true; - }; - openid_client_default_scopes.acme_app = { - realm = "acme"; - client = "acme_app"; - default_scopes = [ - "profile" - "email" - "acme_profile" # managed key, resolves to scope name "acme-profile" - ]; - }; - - # protocol mapper attached to the managed scope by key. - openid_user_attribute_protocol_mappers.team_claim = { - realm = "acme"; - client_scope = "acme_profile"; - name = "team"; - user_attribute = "team"; - claim_name = "team"; - claim_value_type = "String"; - add_to_id_token = true; - add_to_access_token = true; - add_to_userinfo = true; - }; - }; + runtime = fixtures.clients; }; testScript = '' @@ -365,168 +255,7 @@ in containers.keycloak = mkHost { extraEtc."acme-smtp-password".text = "verysecretpassword"; - runtime = { - realms.acme = { - display_name = "ACME Corp."; - display_name_html = "ACME Corp."; - # cross-section of the extended realm attrs. - registration_allowed = true; - login_theme = "keycloak"; - ssl_required = "external"; - access_token_lifespan = "10m"; - password_policy = "length(8)"; - attributes."userProfileEnabled" = "true"; - internationalization = { - supported_locales = [ - "en" - "de" - ]; - default_locale = "en"; - }; - # smtp with a nested-secret indirection (auth.passwordFile). - smtp_server = { - host = "smtp.example.com"; - from = "noreply@example.com"; - port = "25"; - from_display_name = "ACME"; - auth = { - username = "noreply"; - passwordFile = "/etc/acme-smtp-password"; - }; - }; - # nested-in-nested block wrap (headers + brute_force_detection - # inside security_defenses). - security_defenses = { - headers = { - x_frame_options = "DENY"; - strict_transport_security = "max-age=63072000; includeSubDomains; preload"; - }; - brute_force_detection = { - permanent_lockout = false; - max_login_failures = 5; - }; - }; - otp_policy = { - type = "totp"; - algorithm = "HmacSHA256"; - digits = 6; - period = 30; - initial_counter = 0; - look_ahead_window = 1; - }; - }; - - realm_keystore_rsa_generateds.acme_extra_rsa = { - realm = "acme"; - name = "acme-extra-rsa"; - algorithm = "RS256"; - key_size = 2048; - priority = 50; - }; - - required_actions.acme_configure_totp = { - realm = "acme"; - alias = "CONFIGURE_TOTP"; - enabled = false; - default_action = false; - }; - - realm_localizations.acme_en = { - realm = "acme"; - locale = "en"; - texts.loginAccountTitle = "ACME"; - }; - - # realm_user_profile exercises a nested MaxItems:1 block inside a - # list element (attribute[].permissions). keycloak refuses to drop - # the built-in attrs, so declare them alongside the custom one. - realm_user_profiles.acme = { - realm = "acme"; - unmanaged_attribute_policy = "ENABLED"; - attribute = [ - { - name = "username"; - permissions = { - view = [ - "admin" - "user" - ]; - edit = [ - "admin" - "user" - ]; - }; - validator = [ - { - name = "length"; - config = { - min = "3"; - max = "255"; - }; - } - ]; - } - { - name = "email"; - permissions = { - view = [ - "admin" - "user" - ]; - edit = [ - "admin" - "user" - ]; - }; - } - { - name = "firstName"; - permissions = { - view = [ - "admin" - "user" - ]; - edit = [ - "admin" - "user" - ]; - }; - } - { - name = "lastName"; - permissions = { - view = [ - "admin" - "user" - ]; - edit = [ - "admin" - "user" - ]; - }; - } - { - name = "team"; - display_name = "Team"; - group = "metadata"; - permissions = { - view = [ - "admin" - "user" - ]; - edit = [ "admin" ]; - }; - } - ]; - group = [ - { - name = "metadata"; - display_header = "Metadata"; - display_description = "ACME-internal user metadata"; - } - ]; - }; - }; + runtime = fixtures.realmExtras; }; testScript = '' @@ -606,31 +335,7 @@ in containers.keycloak = mkHost { extraEtc."acme-google-secret".text = "fakesecret"; - runtime = { - realms.acme.display_name = "ACME"; - - # google IdP exercises realm-alias resolution + secret-file indirection. - oidc_google_identity_providers.acme_google = { - realm = "acme"; - client_id = "fake-client-id"; - client_secretFile = "/etc/acme-google-secret"; - }; - - # IdP mapper exercises the multi-target idp-alias ref. - attribute_importer_identity_provider_mappers.google_email = { - realm = "acme"; - identity_provider = "acme_google"; - name = "google-email"; - user_attribute = "email"; - claim_name = "email"; - }; - - authentication_flows.acme_passkey = { - realm = "acme"; - alias = "acme-passkey"; - description = "Passkey login flow"; - }; - }; + runtime = fixtures.idp; }; testScript = '' @@ -674,39 +379,7 @@ in name = "declarative-keycloak-e2e"; containers.keycloak = mkHost { - runtime = { - realms.acme = { - display_name = "ACME"; - login_with_email_allowed = true; - }; - - users.alice = { - realm = "acme"; - username = "alice"; - email = "alice@acme.test"; - first_name = "Alice"; - last_name = "Tester"; - enabled = true; - email_verified = true; - initial_password = { - valueFile = "/etc/secrets/alice-pw"; - temporary = false; - }; - }; - - # PUBLIC client, only direct access grants enabled (the password - # grant doesn't use redirects, so no valid_redirect_uris and - # standard/implicit flow off -- the provider rejects redirect - # URIs without a flow that uses them). - openid_clients.test_app = { - realm = "acme"; - client_id = "test-app"; - name = "Test App"; - access_type = "PUBLIC"; - standard_flow_enabled = false; - direct_access_grants_enabled = true; - }; - }; + runtime = fixtures.e2e; extraEtc = { "secrets/alice-pw".text = "hackme"; }; diff --git a/services/keycloak/fixtures.nix b/services/keycloak/fixtures.nix new file mode 100644 index 0000000..b4f58fd --- /dev/null +++ b/services/keycloak/fixtures.nix @@ -0,0 +1,359 @@ +# `services.keycloak.runtime` fixtures, shared by the VM tests in ./checks.nix +# and by the `keycloak-rendered-fixtures` package. +# +# why the indirection: `keycloak-rendered-fixtures` renders these through the +# real option system and renderer, so the `.tf.json` snapshot that guards +# refactors of the resource surface is produced from exactly the configurations +# the VM tests prove converge against a live Keycloak. +{ + # Core test: one realm, plus the realm its specialisation adds. + core = { + realms.acme = { + display_name = "ACME Corp."; + display_name_html = "ACME Corp."; + }; + }; + coreAddRealm = { + realms.delta = { + display_name = "Delta Realm"; + }; + }; + + # Roles, groups, users and their bindings via managed-key list refs. + rbac = { + realms.acme.display_name = "ACME"; + + roles.acme_engineer = { + realm = "acme"; + name = "engineer"; + description = "ACME engineering role"; + }; + default_roles.acme = { + realm = "acme"; + default_roles = [ + "offline_access" + "uma_authorization" + "acme_engineer" # managed key, resolves to role name "engineer" + ]; + }; + + groups.acme_eng = { + realm = "acme"; + name = "engineering"; + attributes."team" = "infra"; + }; + groups.acme_eng_backend = { + realm = "acme"; + name = "backend"; + parent = "acme_eng"; + }; + group_roles.acme_eng_admins = { + realm = "acme"; + group = "acme_eng"; + role_ids = [ "acme_engineer" ]; # managed key + exhaustive = true; + }; + + users.acme_alice = { + realm = "acme"; + username = "alice"; + email = "alice@acme.example"; + first_name = "Alice"; + last_name = "Anderson"; + email_verified = true; + required_actions = [ "UPDATE_PASSWORD" ]; + }; + user_roles.acme_alice = { + realm = "acme"; + user = "acme_alice"; + role_ids = [ "acme_engineer" ]; + exhaustive = false; + }; + user_groups.acme_alice = { + realm = "acme"; + user = "acme_alice"; + group_ids = [ "acme_eng" ]; # managed key + exhaustive = false; + }; + }; + + # OpenID clients + scopes + protocol mapper + default-scope binding. + clients = { + realms.acme.display_name = "ACME"; + + openid_client_scopes.acme_profile = { + realm = "acme"; + name = "acme-profile"; + description = "ACME profile scope"; + consent_screen_text = "Access your ACME profile"; + include_in_token_scope = true; + gui_order = 10; + }; + + openid_clients.acme_app = { + realm = "acme"; + client_id = "acme-app"; + name = "ACME App"; + access_type = "CONFIDENTIAL"; + client_secretFile = "/etc/acme-app-client-secret"; + standard_flow_enabled = true; + direct_access_grants_enabled = true; + service_accounts_enabled = true; + valid_redirect_uris = [ "https://app.acme.example/*" ]; + web_origins = [ "https://app.acme.example" ]; + consent_required = false; + full_scope_allowed = true; + }; + openid_client_default_scopes.acme_app = { + realm = "acme"; + client = "acme_app"; + default_scopes = [ + "profile" + "email" + "acme_profile" # managed key, resolves to scope name "acme-profile" + ]; + }; + + # protocol mapper attached to the managed scope by key. + openid_user_attribute_protocol_mappers.team_claim = { + realm = "acme"; + client_scope = "acme_profile"; + name = "team"; + user_attribute = "team"; + claim_name = "team"; + claim_value_type = "String"; + add_to_id_token = true; + add_to_access_token = true; + add_to_userinfo = true; + }; + }; + + # Realm extras: extended realm attrs, smtp with nested-secret, + # security_defenses (nested-in-nested), otp_policy, realm_user_profile + # (nested-in-list), a keystore, required_action, localization. + realmExtras = { + realms.acme = { + display_name = "ACME Corp."; + display_name_html = "ACME Corp."; + # cross-section of the extended realm attrs. + registration_allowed = true; + login_theme = "keycloak"; + ssl_required = "external"; + access_token_lifespan = "10m"; + password_policy = "length(8)"; + attributes."userProfileEnabled" = "true"; + internationalization = { + supported_locales = [ + "en" + "de" + ]; + default_locale = "en"; + }; + # smtp with a nested-secret indirection (auth.passwordFile). + smtp_server = { + host = "smtp.example.com"; + from = "noreply@example.com"; + port = "25"; + from_display_name = "ACME"; + auth = { + username = "noreply"; + passwordFile = "/etc/acme-smtp-password"; + }; + }; + # nested-in-nested block wrap (headers + brute_force_detection + # inside security_defenses). + security_defenses = { + headers = { + x_frame_options = "DENY"; + strict_transport_security = "max-age=63072000; includeSubDomains; preload"; + }; + brute_force_detection = { + permanent_lockout = false; + max_login_failures = 5; + }; + }; + otp_policy = { + type = "totp"; + algorithm = "HmacSHA256"; + digits = 6; + period = 30; + initial_counter = 0; + look_ahead_window = 1; + }; + }; + + realm_keystore_rsa_generateds.acme_extra_rsa = { + realm = "acme"; + name = "acme-extra-rsa"; + algorithm = "RS256"; + key_size = 2048; + priority = 50; + }; + + required_actions.acme_configure_totp = { + realm = "acme"; + alias = "CONFIGURE_TOTP"; + enabled = false; + default_action = false; + }; + + realm_localizations.acme_en = { + realm = "acme"; + locale = "en"; + texts.loginAccountTitle = "ACME"; + }; + + # realm_user_profile exercises a nested MaxItems:1 block inside a + # list element (attribute[].permissions). keycloak refuses to drop + # the built-in attrs, so declare them alongside the custom one. + realm_user_profiles.acme = { + realm = "acme"; + unmanaged_attribute_policy = "ENABLED"; + attribute = [ + { + name = "username"; + permissions = { + view = [ + "admin" + "user" + ]; + edit = [ + "admin" + "user" + ]; + }; + validator = [ + { + name = "length"; + config = { + min = "3"; + max = "255"; + }; + } + ]; + } + { + name = "email"; + permissions = { + view = [ + "admin" + "user" + ]; + edit = [ + "admin" + "user" + ]; + }; + } + { + name = "firstName"; + permissions = { + view = [ + "admin" + "user" + ]; + edit = [ + "admin" + "user" + ]; + }; + } + { + name = "lastName"; + permissions = { + view = [ + "admin" + "user" + ]; + edit = [ + "admin" + "user" + ]; + }; + } + { + name = "team"; + display_name = "Team"; + group = "metadata"; + permissions = { + view = [ + "admin" + "user" + ]; + edit = [ "admin" ]; + }; + } + ]; + group = [ + { + name = "metadata"; + display_header = "Metadata"; + display_description = "ACME-internal user metadata"; + } + ]; + }; + }; + + # Identity providers + IdP mappers + an authentication flow. + idp = { + realms.acme.display_name = "ACME"; + + # google IdP exercises realm-alias resolution + secret-file indirection. + oidc_google_identity_providers.acme_google = { + realm = "acme"; + client_id = "fake-client-id"; + client_secretFile = "/etc/acme-google-secret"; + }; + + # IdP mapper exercises the multi-target idp-alias ref. + attribute_importer_identity_provider_mappers.google_email = { + realm = "acme"; + identity_provider = "acme_google"; + name = "google-email"; + user_attribute = "email"; + claim_name = "email"; + }; + + authentication_flows.acme_passkey = { + realm = "acme"; + alias = "acme-passkey"; + description = "Passkey login flow"; + }; + }; + + # OIDC password-grant end-to-end: a declared user authenticating against a + # declared client. + e2e = { + realms.acme = { + display_name = "ACME"; + login_with_email_allowed = true; + }; + + users.alice = { + realm = "acme"; + username = "alice"; + email = "alice@acme.test"; + first_name = "Alice"; + last_name = "Tester"; + enabled = true; + email_verified = true; + initial_password = { + valueFile = "/etc/secrets/alice-pw"; + temporary = false; + }; + }; + + # PUBLIC client, only direct access grants enabled (the password + # grant doesn't use redirects, so no valid_redirect_uris and + # standard/implicit flow off -- the provider rejects redirect + # URIs without a flow that uses them). + openid_clients.test_app = { + realm = "acme"; + client_id = "test-app"; + name = "Test App"; + access_type = "PUBLIC"; + standard_flow_enabled = false; + direct_access_grants_enabled = true; + }; + }; +} From 5bcc4b306616ca136b0664a465adcd9e0447a780 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 18:03:22 +0200 Subject: [PATCH 14/18] refactor(services/keycloak): derive resourceTypes from the vendored schema `services/keycloak/lib.nix` now builds its 97 resource collections with `modules/lib/tf-schema.nix` from `services/keycloak/provider-schema.json` instead of declaring every attribute by hand. What stays hand-written is what the provider schema cannot express: the NixOS-facing resource descriptions, the reference graph, and the per-resource corrections (`omit`, `extraSecrets`, `requiredAttrs`). The generator refuses to ignore a schema resource silently, so eval is only green once every resource is either modelled or listed in `unsupported` with a reason. That set -- the four resources new in provider 5.8.0 -- therefore lands here rather than in the follow-up, and its four entries are the complete statement of what this pairing does not cover. `omitEverywhere = [ "id" ]` is new in the generator: every sdk/v2 resource declares a synthetic `id`, which is the resource's own identity, computed on apply, and nothing a configuration declares. It is a provider-wide dialect artifact, not a per-resource correction, and a global check reports it as stale once no modelled resource declares it. Two references the hand-written surface lacked: `roles.client` (a role scoped to a client rather than to the realm) and `groups.organization`. Verification: - Rendered fixtures (`nix build .#keycloak-rendered-fixtures`) are byte-identical to the pre-refactor snapshot except `smtp_server.auth`, which now renders as `[ { ... } ]`. That is the deliberate correctness fix: the schema marks it `nesting_mode: "list", max_items: 1`, so Terraform reads it as a one-element list, and the hand-written `blockAttrs` list omitted it. Nothing else changed and nothing was removed. - Options doc: 1217 -> 1238 options; 22 added, 1 removed, 917 descriptions changed, 124 types changed, 81 became required. The single removal, `kubernetes_identity_providers..hide_on_login_page`, is drift the schema catches -- the provider marks it computed-only ("This is always set to true for Kubernetes identity provider."). Type changes are `int` -> `number` widenings plus one fix (`ldap_user_federations..cache.eviction_day` was typed `str`). - `nix flake check` green, including all five keycloak VM tests. Assisted-by: Claude:claude-opus-5 --- flake.nix | 5 +- modules/lib/schema-report.nix | 9 +- modules/lib/tf-schema.nix | 27 +- services/keycloak/lib.nix | 4411 ++++++++++++--------------------- services/keycloak/module.nix | 6 +- 5 files changed, 1592 insertions(+), 2866 deletions(-) diff --git a/flake.nix b/flake.nix index 50030a0..6fabb5b 100644 --- a/flake.nix +++ b/flake.nix @@ -42,7 +42,10 @@ inherit pkgs; nixTfSchema = inputs.nix-tf-schema; }; - keycloak = import ./services/keycloak/lib.nix { inherit pkgs; }; + keycloak = import ./services/keycloak/lib.nix { + inherit pkgs; + nixTfSchema = inputs.nix-tf-schema; + }; }; # A NixOS module cannot reach a flake input by path, so the schema library diff --git a/modules/lib/schema-report.nix b/modules/lib/schema-report.nix index e4b26b8..a2a9e28 100644 --- a/modules/lib/schema-report.nix +++ b/modules/lib/schema-report.nix @@ -47,9 +47,12 @@ in `Options` counts a collection's top-level options against the settable top-level attributes the provider schema declares for it; the two differ - exactly by the attributes consumed by references and by `Omitted`. Nested - block attributes are options of their own submodule and are not counted - here. + exactly by the attributes consumed by references and by the dropped ones. + Nested block attributes are options of their own submodule and are not + counted here. + ${lib.optionalString (coverage.omitEverywhere != [ ]) + "Dropped from every collection that declares them, on top of each `Omitted` column: ${cell coverage.omitEverywhere}." + } ## Modelled diff --git a/modules/lib/tf-schema.nix b/modules/lib/tf-schema.nix index 556280f..04d477e 100644 --- a/modules/lib/tf-schema.nix +++ b/modules/lib/tf-schema.nix @@ -69,6 +69,10 @@ in runtimePrefix "services..runtime" -- for error messages resources collection name -> overlay (see below) unsupported schema resource type -> non-empty reason for not modelling it + omitEverywhere dotted paths dropped from every collection that declares + them. For dialect artifacts that repeat across the whole + provider -- the sdk/v2 synthetic `id`, say -- not as a + shortcut for per-collection `omit`. An overlay's first six fields are mandatory; the rest default to empty and exist only to correct things a schema cannot state. There is deliberately no @@ -103,6 +107,7 @@ in runtimePrefix, resources, unsupported ? { }, + omitEverywhere ? [ ], }: let check = cond: msg: if cond then null else throw "${runtimePrefix}: ${msg}"; @@ -143,7 +148,10 @@ in # collection key and generation fills these in, so they must not also # be settable options. refConsumed = mapAttrsToList (_: r: r.attr) o.refs; - droppedPaths = o.omit ++ refConsumed; + # the global list is filtered to what this resource has, so it needs + # no per-collection opt-in; the check that it names something real + # runs once, provider-wide. + droppedPaths = o.omit ++ refConsumed ++ filter (p: paths ? ${p}) omitEverywhere; isDropped = path: lib.any (d: path == d || lib.hasPrefix "${d}." path) droppedPaths; isSecret = @@ -368,7 +376,12 @@ in }; in { - inherit spec checks coverage; + inherit + spec + checks + coverage + allPaths + ; }; built = lib.mapAttrs mkOne resources; @@ -384,6 +397,11 @@ in unclaimed = subtractLists (allTypes ++ unsupportedTypes) schemaTypes; + # a path is worth omitting provider-wide only while some resource still + # declares it; once none does, the entry is stale. + claimedPaths = unique (lib.concatLists (mapAttrsToList (_: r: r.allPaths) built)); + staleOmit = subtractLists claimedPaths omitEverywhere; + globalChecks = [ # identity: the cheap guards that fire the instant nixpkgs moves the # provider under us. `-schema-current` is the authoritative check @@ -396,6 +414,10 @@ in ) (check (elem schema.format_version knownFormatVersions) "unrecognized schema `format_version` `${schema.format_version}` (known: ${quoteList knownFormatVersions})") + (check (staleOmit == [ ]) + "`omitEverywhere` lists ${quoteList staleOmit}, which no modelled resource of ${source} ${provider.version} declares" + ) + (check ( duplicates allTypes == [ ] ) "resource ${quoteList (duplicates allTypes)} claimed by more than one collection") @@ -437,6 +459,7 @@ in source runtimePrefix unsupported + omitEverywhere ; inherit (provider) version; schemaResources = length schemaTypes; diff --git a/services/keycloak/lib.nix b/services/keycloak/lib.nix index 3c54f6b..daa95c6 100644 --- a/services/keycloak/lib.nix +++ b/services/keycloak/lib.nix @@ -1,24 +1,23 @@ -# keycloak-provider specifics: executor, resource types, provider block. -# shared helpers (option helpers, renderer, reconciler) live in modules/lib. -{ pkgs }: +# keycloak-provider specifics: executor, resource surface, provider block. +# +# The resource surface is *derived* from the vendored provider schema +# (./provider-schema.json, parsed by ./schema.nix) via +# ../../modules/lib/tf-schema.nix. What stays hand-written is only what a schema +# cannot state: the NixOS-facing collection descriptions, the reference graph +# between collections, and the odd documented correction. A provider bump that +# adds, removes or retypes anything is then an eval-time error rather than an +# apply-time surprise. +# +# Shared helpers (option helpers, renderer, reconciler) live in modules/lib. +{ pkgs, nixTfSchema }: let - inherit (pkgs) lib; genlib = import ../../modules/lib { inherit pkgs; }; - inherit (genlib) - oStr - oBool - oInt - oListStr - oAttrsStr - oSub - oListSub - rStr - ; - + tfSchema = import ../../modules/lib/tf-schema.nix { inherit pkgs nixTfSchema; }; provider = pkgs.terraform-providers.keycloak_keycloak; providerVersion = provider.version; # provider source address; also keys the vendored provider schema. providerSource = "keycloak/keycloak"; + runtimePrefix = "services.keycloak.runtime"; # tf-var names for the service-account oauth2 client the reconciler uses. tokenVar = "keycloak_client_secret"; @@ -67,14 +66,6 @@ let required = false; description = "Optional managed OpenID client scope this mapper attaches to."; }; - # attrs every openid mapper carries (some carry only the first 3). - openidMapperCommonAttrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - add_to_id_token = oBool "Include in ID token?"; - add_to_access_token = oBool "Include in access token?"; - add_to_userinfo = oBool "Include in UserInfo?"; - }; - # protocol mappers attach to a client *or* a client scope -- never # both, never neither (the provider rejects either). clientOrScopeOneOf = [ @@ -174,32 +165,6 @@ let description = "Alias of the managed identity provider (in any IdP collection) this mapper attaches to, or a literal alias."; }; - # attrs every IdP mapper carries. - commonIdpMapperAttrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - extra_config = oAttrsStr "Free-form extra mapper config entries."; - }; - - # attrs every IdP exposes (alias is the IdP key, etc.). - commonIdpAttrs = { - alias = oStr "Provider alias. Defaults to the attribute key."; - display_name = oStr "Human-readable name shown on the login page."; - enabled = oBool "Is the identity provider enabled?"; - store_token = oBool "Persist tokens obtained from the IdP."; - add_read_token_role_on_create = oBool "Grant the read-token role to newly federated users."; - authenticate_by_default = oBool "Use this IdP as the default authenticator."; - link_only = oBool "Don't allow new login -- only link existing accounts."; - trust_email = oBool "Trust the email returned by the IdP (skip verification)."; - first_broker_login_flow_alias = oStr "Alias of the first-broker-login flow used."; - post_broker_login_flow_alias = oStr "Alias of the post-broker-login flow used."; - organization_id = oStr "Optional organization id this IdP belongs to."; - extra_config = oAttrsStr "Free-form extra IdP config entries."; - gui_order = oStr "Display order in the admin UI (string)."; - sync_mode = oStr "Sync mode: 'IMPORT', 'LEGACY', or 'FORCE'."; - org_redirect_mode_email_matches = oBool "Redirect users whose email matches an organization's domain to this IdP."; - org_domain = oStr "Organization domain matched against the user's email."; - }; - # generic mappers attach to either an openid or a saml client/scope. # multi-target: a managed key from either collection resolves; an # unknown string falls through as a literal. @@ -235,2824 +200,1551 @@ let required = false; description = "Optional managed client scope (openid or saml) this mapper attaches to."; }; - - # every keycloak resource type we expose. each entry: - # type `keycloak_*` resource name - # prefix tf-label prefix - # nameAttr attribute defaulted from the collection key (or null) - # scope reserved; currently unused - # refs parent links resolved to managed siblings - # blockAttrs dotted paths that wrap as `[ {...} ]` (MaxItems:1) - # secrets attrs that gain an `File` sibling - # requiredSecrets secrets that must be set as literal or File - # requiredAttrs attrs that must be set non-empty - # attrs settable attributes, all typed (no freeform) - resourceTypes = { - realms = { - type = "keycloak_realm"; - prefix = "realm"; - nameAttr = "realm"; - scope = null; - refs = { }; - blockAttrs = [ - "smtp_server" - "internationalization" - "security_defenses" - "security_defenses.headers" - "security_defenses.brute_force_detection" - "otp_policy" - "web_authn_policy" - "web_authn_passwordless_policy" - ]; - description = "Keycloak realms, keyed by realm name."; - attrs = { - realm = oStr "Realm name. Defaults to the attribute key."; - enabled = oBool "Is the realm enabled?"; - display_name = oStr "User-facing display name."; - display_name_html = oStr "HTML-formatted display name."; - - # general - user_managed_access = oBool "Enable user-managed access."; - organizations_enabled = oBool "Enable the organizations feature."; - admin_permissions_enabled = oBool "Enable the v2 admin permissions feature."; - terraform_deletion_protection = oBool "Refuse to destroy the realm on `tofu destroy`."; - attributes = oAttrsStr "Free-form realm attribute map."; - - # login config - registration_allowed = oBool "Allow self-registration."; - registration_email_as_username = oBool "Use email as username on registration."; - edit_username_allowed = oBool "Allow users to edit their username."; - reset_password_allowed = oBool "Allow users to reset their password."; - remember_me = oBool "Offer the \"Remember Me\" checkbox on login."; - verify_email = oBool "Require email verification."; - login_with_email_allowed = oBool "Allow login with email."; - duplicate_emails_allowed = oBool "Allow duplicate emails across users."; - ssl_required = oStr "SSL required: 'none', 'external' (default), or 'all'."; - - # themes - login_theme = oStr "Login theme."; - account_theme = oStr "Account console theme."; - admin_theme = oStr "Admin console theme."; - email_theme = oStr "Email theme."; - - # tokens - default_signature_algorithm = oStr "Default JWS signing algorithm."; - revoke_refresh_token = oBool "Revoke refresh tokens on use."; - refresh_token_max_reuse = oInt "Max number of times a refresh token can be reused."; - sso_session_idle_timeout = oStr "SSO session idle timeout (duration string, e.g. \"30m\")."; - sso_session_idle_timeout_remember_me = oStr "SSO session idle timeout for \"Remember Me\" sessions."; - sso_session_max_lifespan = oStr "SSO session max lifespan."; - sso_session_max_lifespan_remember_me = oStr "SSO session max lifespan for \"Remember Me\" sessions."; - offline_session_idle_timeout = oStr "Offline session idle timeout."; - offline_session_max_lifespan = oStr "Offline session max lifespan."; - offline_session_max_lifespan_enabled = oBool "Cap offline sessions to `offline_session_max_lifespan`."; - client_session_idle_timeout = oStr "Client session idle timeout (falls back to SSO idle)."; - client_session_max_lifespan = oStr "Client session max lifespan (falls back to SSO max)."; - access_token_lifespan = oStr "Access token lifespan."; - access_token_lifespan_for_implicit_flow = oStr "Access token lifespan for the implicit flow."; - access_code_lifespan = oStr "Auth code lifespan."; - access_code_lifespan_login = oStr "Login-action code lifespan."; - access_code_lifespan_user_action = oStr "User-action code lifespan."; - action_token_generated_by_user_lifespan = oStr "Lifespan of user-generated action tokens."; - action_token_generated_by_admin_lifespan = oStr "Lifespan of admin-generated action tokens."; - oauth2_device_code_lifespan = oStr "OAuth2 device-code lifespan."; - oauth2_device_polling_interval = oInt "OAuth2 device-code polling interval (seconds)."; - - # authentication - password_policy = oStr "Password policy string (e.g. \"upperCase(1) and length(8) and notUsername(undefined)\")."; - - # authentication flow bindings (alias of a flow defined in the realm) - browser_flow = oStr "Authentication flow alias bound to the browser flow."; - registration_flow = oStr "Authentication flow alias bound to the registration flow."; - direct_grant_flow = oStr "Authentication flow alias bound to the direct-grant flow."; - reset_credentials_flow = oStr "Authentication flow alias bound to the reset-credentials flow."; - client_authentication_flow = oStr "Authentication flow alias bound to the client-auth flow."; - docker_authentication_flow = oStr "Authentication flow alias bound to the docker-auth flow."; - first_broker_login_flow = oStr "Authentication flow alias bound to the first-broker-login flow."; - - # default client scopes (referenced by name) - default_default_client_scopes = oListStr "Default client scopes auto-granted to new clients."; - default_optional_client_scopes = oListStr "Optional client scopes available to new clients."; - - # nested blocks; emitted as `[{ ... }]` via blockAttrs. - # nested secrets (smtp.auth.password, smtp.token_auth.client_secret) - # accept either a literal or an `File` host path. - smtp_server = oSub { - host = rStr "SMTP host."; - from = rStr "From address."; - port = oStr "SMTP port (string -- matches the provider schema)."; - starttls = oBool "Use STARTTLS."; - ssl = oBool "Use SSL/TLS."; - allow_utf8 = oBool "Allow UTF-8 in addresses."; - from_display_name = oStr "Display name shown on the From: line."; - reply_to = oStr "Reply-to address."; - reply_to_display_name = oStr "Reply-to display name."; - envelope_from = oStr "Envelope From address."; - auth = oSub { - username = rStr "SMTP auth username."; - password = oStr "SMTP auth password. Prefer `passwordFile`."; - passwordFile = oStr "Runtime path to a file holding `password` (loaded via systemd LoadCredential=; never copied to the store). Mutually exclusive with a literal `password`."; - } "SMTP basic-auth credentials (mutually exclusive with token_auth)."; - token_auth = oSub { - username = rStr "OAuth2 token-auth username."; - url = rStr "OAuth2 token endpoint."; - client_id = rStr "OAuth2 client_id."; - client_secret = oStr "OAuth2 client_secret. Prefer `client_secretFile`."; - client_secretFile = oStr "Runtime path to a file holding `client_secret` (loaded via systemd LoadCredential=; never copied to the store). Mutually exclusive with a literal `client_secret`."; - scope = rStr "OAuth2 scope."; - } "SMTP OAuth2 token credentials (mutually exclusive with auth)."; - } "SMTP server configuration."; - - internationalization = oSub { - supported_locales = lib.mkOption { - type = lib.types.listOf lib.types.str; - description = "Locales the realm supports."; + # The keycloak/keycloak resource surface. Per collection, only the facts the + # schema does not carry (see modules/lib/tf-schema.nix for the full overlay + # vocabulary): + # type the `keycloak_*` resource type in the schema + # prefix unique Terraform label prefix + # nameAttr attribute defaulted from the collection key (or null) + # scope unused here -- keycloak authenticates a service account, not + # a scoped token + # refs parent links resolved to references against managed siblings + # description the collection's NixOS option description + generated = tfSchema.mkResourceTypes { + schema = import ./schema.nix; + inherit provider runtimePrefix; + source = providerSource; + # every sdk/v2 resource carries a synthetic `id`; it is the resource's own + # identity, computed on apply, and nothing a configuration declares. + omitEverywhere = [ "id" ]; + # resources the provider offers that this pairing does not model yet. The + # generator refuses to ignore a resource silently, so this list is the + # complete, reviewable statement of what is missing. + unsupported = { + keycloak_oidc_openshift_v4_identity_provider = "New in provider 5.8.0; not modelled yet."; + keycloak_openid_client_regex_policy = "New in provider 5.8.0; not modelled yet."; + keycloak_spiffe_identity_provider = "New in provider 5.8.0; not modelled yet."; + keycloak_workflow = "New in provider 5.8.0; not modelled yet."; + }; + resources = { + realms = { + type = "keycloak_realm"; + prefix = "realm"; + nameAttr = "realm"; + scope = null; + refs = { }; + description = "Keycloak realms, keyed by realm name."; + # Computed identity attribute; nothing to declare. + omit = [ "internal_id" ]; + }; + roles = { + type = "keycloak_role"; + prefix = "role"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + composite_roles = { + attr = "composite_roles"; + targets = [ + { + collection = "roles"; + field = "id"; + } + ]; + managedOnly = false; + required = false; + list = true; + description = "Roles composited into this role. Each entry is a managed role key (resolved to its id) or a literal role UUID."; }; - default_locale = rStr "Default locale."; - } "Realm internationalization settings."; - - security_defenses = oSub { - headers = oSub { - x_frame_options = oStr "X-Frame-Options header value."; - content_security_policy = oStr "Content-Security-Policy header value."; - content_security_policy_report_only = oStr "Content-Security-Policy-Report-Only header value."; - x_content_type_options = oStr "X-Content-Type-Options header value."; - x_robots_tag = oStr "X-Robots-Tag header value."; - x_xss_protection = oStr "X-XSS-Protection header value."; - strict_transport_security = oStr "Strict-Transport-Security header value."; - referrer_policy = oStr "Referrer-Policy header value."; - } "Response-header defaults Keycloak applies to admin/account endpoints."; - brute_force_detection = oSub { - permanent_lockout = oBool "Permanently lock accounts after too many failures."; - max_temporary_lockouts = oInt "Max number of temporary lockouts before a permanent one."; - max_login_failures = oInt "Number of failures triggering a lockout."; - wait_increment_seconds = oInt "Lockout duration increment."; - quick_login_check_milli_seconds = oInt "Quick-login check window (ms)."; - minimum_quick_login_wait_seconds = oInt "Minimum wait after a quick-login failure."; - max_failure_wait_seconds = oInt "Maximum lockout duration."; - failure_reset_time_seconds = oInt "Failure counter reset window."; - } "Brute-force-protection settings."; - } "Security defenses (response headers + brute-force protection)."; - - otp_policy = oSub { - type = oStr "OTP type: 'totp' (default) or 'hotp'."; - algorithm = oStr "HMAC algorithm: 'HmacSHA1' (default), 'HmacSHA256', or 'HmacSHA512'."; - digits = oInt "Number of OTP digits (6 or 8)."; - initial_counter = oInt "Initial counter (HOTP)."; - look_ahead_window = oInt "Look-ahead window size."; - period = oInt "Time-step (TOTP) in seconds."; - } "Realm OTP policy."; - - web_authn_policy = oSub { - acceptable_aaguids = oListStr "Accepted authenticator AAGUIDs (empty = any)."; - extra_origins = oListStr "Extra trusted origins for WebAuthn registration / login."; - attestation_conveyance_preference = oStr "Attestation conveyance preference ('not specified', 'none', 'indirect', 'direct')."; - authenticator_attachment = oStr "Authenticator attachment ('not specified', 'platform', 'cross-platform')."; - avoid_same_authenticator_register = oBool "Refuse to register an already-registered authenticator."; - create_timeout = oInt "Registration ceremony timeout in seconds."; - require_resident_key = oStr "Require a resident key ('not specified', 'Yes', 'No')."; - relying_party_entity_name = oStr "Relying-Party entity name."; - relying_party_id = oStr "Relying-Party id."; - signature_algorithms = oListStr "COSEAlgorithmIdentifiers accepted."; - user_verification_requirement = oStr "User verification requirement ('not specified', 'required', 'preferred', 'discouraged')."; - } "Realm WebAuthn (second-factor) policy."; - - web_authn_passwordless_policy = oSub { - acceptable_aaguids = oListStr "Accepted authenticator AAGUIDs (empty = any)."; - extra_origins = oListStr "Extra trusted origins for WebAuthn registration / login."; - attestation_conveyance_preference = oStr "Attestation conveyance preference."; - authenticator_attachment = oStr "Authenticator attachment."; - avoid_same_authenticator_register = oBool "Refuse to register an already-registered authenticator."; - create_timeout = oInt "Registration ceremony timeout in seconds."; - require_resident_key = oStr "Require a resident key."; - relying_party_entity_name = oStr "Relying-Party entity name."; - relying_party_id = oStr "Relying-Party id."; - signature_algorithms = oListStr "COSEAlgorithmIdentifiers accepted."; - user_verification_requirement = oStr "User verification requirement."; - } "Realm WebAuthn passwordless policy."; - }; - }; - - roles = { - type = "keycloak_role"; - prefix = "role"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - composite_roles = { - attr = "composite_roles"; - targets = [ - { - collection = "roles"; - field = "id"; - } - ]; - managedOnly = false; - required = false; - list = true; - description = "Roles composited into this role. Each entry is a managed role key (resolved to its id) or a literal role UUID."; - }; - }; - description = "Keycloak roles (realm-level by default), keyed by role name."; - attrs = { - name = oStr "Role name. Defaults to the attribute key."; - description = oStr "Role description."; - attributes = oAttrsStr "Free-form role attribute map."; - }; - }; - - default_roles = { - type = "keycloak_default_roles"; - prefix = "default_roles"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - default_roles = { - attr = "default_roles"; - targets = [ - { - collection = "roles"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Role names auto-granted to every new user. Each entry is a managed role key (resolved to its name) or a literal role name (built-ins like 'offline_access' work as literals)."; - }; - }; - description = "Realm-level default roles auto-granted to new users, keyed by an arbitrary label."; - attrs = { }; - }; - - groups = { - type = "keycloak_group"; - prefix = "group"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - parent = { - attr = "parent_id"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = true; - required = false; - description = "Optional parent group (key of another managed group) for nested groups."; - }; - }; - description = "Keycloak groups, keyed by group name."; - attrs = { - name = oStr "Group name. Defaults to the attribute key."; - description = oStr "Group description."; - attributes = oAttrsStr "Free-form group attribute map."; - }; - }; - - default_groups = { - type = "keycloak_default_groups"; - prefix = "default_groups"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - group_ids = { - attr = "group_ids"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Groups new users auto-join. Each entry is a managed group key (resolved to its id) or a literal group UUID."; - }; - }; - description = "Realm-level default groups auto-joined by new users, keyed by an arbitrary label."; - attrs = { }; - }; - - group_memberships = { - type = "keycloak_group_memberships"; - prefix = "group_membership"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - group = { - attr = "group_id"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed group (services.keycloak.runtime.groups.) the members are added to."; - }; - members = { - attr = "members"; - targets = [ - { - collection = "users"; - field = "username"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Users to add to the group. Each entry is a managed user key (resolved to its username) or a literal username."; - }; - }; - description = "Keycloak group memberships, keyed by an arbitrary label."; - attrs = { }; - }; - - group_roles = { - type = "keycloak_group_roles"; - prefix = "group_roles"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - group = { - attr = "group_id"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed group (services.keycloak.runtime.groups.) to assign roles to."; - }; - role_ids = { - attr = "role_ids"; - targets = [ - { - collection = "roles"; - field = "id"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Roles granted to the group. Each entry is a managed role key (resolved to its id) or a literal role UUID."; - }; - }; - description = "Role assignments for a group, keyed by an arbitrary label."; - attrs = { - exhaustive = oBool "If true, only the listed roles remain assigned; if false, listed roles are added without removing others."; - }; - }; - - users = { - type = "keycloak_user"; - prefix = "user"; - nameAttr = "username"; - scope = null; - refs.realm = realmRef; - requiredAttrs = [ "username" ]; - blockAttrs = [ "initial_password" ]; - # initial_password.value supports the `valueFile` indirection; - # federated_identity is a list of nested blocks (rendered as a - # JSON array, no wrap needed). - description = "Keycloak users, keyed by username (must be lowercase)."; - attrs = { - username = oStr "Username (lowercase). Defaults to the attribute key."; - email = oStr "Email address."; - email_verified = oBool "Has the user verified their email?"; - first_name = oStr "First name."; - last_name = oStr "Last name."; - enabled = oBool "Is the user enabled?"; - attributes = oAttrsStr "Free-form user attribute map."; - required_actions = oListStr "Required actions on next login (e.g. \"VERIFY_EMAIL\", \"UPDATE_PASSWORD\")."; - - initial_password = oSub { - value = oStr "Initial password literal. Prefer `valueFile`."; - valueFile = oStr "Runtime path to a file holding `value` (loaded via systemd LoadCredential=; never copied to the store). Mutually exclusive with a literal `value`."; - temporary = oBool "Force the user to change the password on first login."; - } "Initial password set at user creation."; - - federated_identity = - oListSub - { - identity_provider = rStr "Alias of the federating IdP."; - user_id = rStr "User id on the IdP side."; - user_name = rStr "Username on the IdP side."; - } - "Federated-identity links pre-bound to the user; each block is `{ identity_provider; user_id; user_name; }`."; - }; - }; - - user_roles = { - type = "keycloak_user_roles"; - prefix = "user_roles"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - user = { - attr = "user_id"; - targets = [ - { - collection = "users"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed user (services.keycloak.runtime.users.) to assign roles to."; - }; - role_ids = { - attr = "role_ids"; - targets = [ - { - collection = "roles"; - field = "id"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Roles granted to the user. Each entry is a managed role key (resolved to its id) or a literal role UUID."; - }; - }; - description = "Role assignments for a user, keyed by an arbitrary label."; - attrs = { - exhaustive = oBool "If true, only the listed roles remain assigned; otherwise the listed roles are added without removing others."; - }; - }; - - user_groups = { - type = "keycloak_user_groups"; - prefix = "user_groups"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - user = { - attr = "user_id"; - targets = [ - { - collection = "users"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed user (services.keycloak.runtime.users.) to add to groups."; - }; - group_ids = { - attr = "group_ids"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Groups the user joins. Each entry is a managed group key (resolved to its id) or a literal group UUID."; - }; - }; - description = "Group memberships for a user, keyed by an arbitrary label."; - attrs = { - exhaustive = oBool "If true, only the listed groups remain joined; otherwise the listed groups are added without removing others."; - }; - }; - - openid_client_scopes = { - type = "keycloak_openid_client_scope"; - prefix = "openid_client_scope"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "OpenID client scopes (per-realm), keyed by scope name."; - attrs = { - name = oStr "Scope name. Defaults to the attribute key."; - description = oStr "Scope description."; - consent_screen_text = oStr "Text shown on the consent screen."; - include_in_token_scope = oBool "Include the scope name in the issued token's `scope` claim?"; - gui_order = oInt "Display order in the admin UI."; - extra_config = oAttrsStr "Free-form extra config entries the upstream attribute set does not cover."; - }; - }; - - saml_client_scopes = { - type = "keycloak_saml_client_scope"; - prefix = "saml_client_scope"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "SAML client scopes (per-realm), keyed by scope name."; - attrs = { - name = oStr "Scope name. Defaults to the attribute key."; - description = oStr "Scope description."; - consent_screen_text = oStr "Text shown on the consent screen."; - gui_order = oInt "Display order in the admin UI."; - extra_config = oAttrsStr "Free-form extra config entries the upstream attribute set does not cover."; - }; - }; - - openid_clients = { - type = "keycloak_openid_client"; - prefix = "openid_client"; - nameAttr = "client_id"; - scope = null; - refs.realm = realmRef; - secrets = [ "client_secret" ]; - blockAttrs = [ - "authorization" - "authentication_flow_binding_overrides" - ]; - # write-only secret variants (client_secret_wo / - # client_secret_wo_version) are skipped -- they need a separate - # write-only renderer mode. - description = "OpenID Connect clients (per-realm), keyed by clientId."; - attrs = { - client_id = oStr "OAuth2 clientId. Defaults to the attribute key."; - name = oStr "Display name."; - description = oStr "Client description."; - enabled = oBool "Is the client enabled?"; - access_type = oStr "Access type: 'CONFIDENTIAL', 'PUBLIC', or 'BEARER-ONLY'."; - - client_secret = oStr "Client secret. Prefer `client_secretFile` to keep it out of the world-readable store."; - client_authenticator_type = oStr "Client authenticator type (default 'client-secret')."; - - standard_flow_enabled = oBool "Enable the standard (authorization code) flow."; - implicit_flow_enabled = oBool "Enable the implicit flow."; - direct_access_grants_enabled = oBool "Enable direct-access (password) grants."; - service_accounts_enabled = oBool "Enable a service account for client-credentials grants."; - frontchannel_logout_enabled = oBool "Enable front-channel logout."; - - valid_redirect_uris = oListStr "Valid redirect URIs (sets/wildcards allowed)."; - valid_post_logout_redirect_uris = oListStr "Valid post-logout redirect URIs."; - web_origins = oListStr "Allowed CORS origins."; - - root_url = oStr "Root URL."; - admin_url = oStr "Admin URL."; - base_url = oStr "Base URL."; - login_theme = oStr "Per-client login theme."; - - pkce_code_challenge_method = oStr "PKCE code-challenge method (e.g. 'S256')."; - require_dpop_bound_tokens = oBool "Require DPoP-bound tokens."; - - access_token_lifespan = oStr "Override realm-level access token lifespan."; - client_offline_session_idle_timeout = oStr "Override realm-level offline-session idle timeout."; - client_offline_session_max_lifespan = oStr "Override realm-level offline-session max lifespan."; - client_session_idle_timeout = oStr "Override realm-level client-session idle timeout."; - client_session_max_lifespan = oStr "Override realm-level client-session max lifespan."; - - exclude_session_state_from_auth_response = oBool "Exclude session_state from auth responses."; - exclude_issuer_from_auth_response = oBool "Exclude issuer from auth responses."; - - full_scope_allowed = oBool "Grant the full scope by default."; - consent_required = oBool "Require consent on first use."; - display_on_consent_screen = oBool "Display the client on the consent screen."; - consent_screen_text = oStr "Text shown on the consent screen."; - - use_refresh_tokens = oBool "Issue refresh tokens."; - use_refresh_tokens_client_credentials = oBool "Issue refresh tokens for client-credentials grants."; - standard_token_exchange_enabled = oBool "Enable standard token exchange."; - allow_refresh_token_in_standard_token_exchange = oStr "Refresh-token policy for standard token exchange ('NO', 'SAME_SESSION', 'YES')."; - - frontchannel_logout_url = oStr "Front-channel logout URL."; - backchannel_logout_url = oStr "Back-channel logout URL."; - backchannel_logout_session_required = oBool "Include session_id in back-channel logout requests."; - backchannel_logout_revoke_offline_sessions = oBool "Revoke offline sessions on back-channel logout."; - - oauth2_device_authorization_grant_enabled = oBool "Enable the OAuth2 device authorization grant."; - oauth2_device_code_lifespan = oStr "Device code lifespan."; - oauth2_device_polling_interval = oStr "Device polling interval."; - - always_display_in_console = oBool "Always display the client in the user account console."; - extra_config = oAttrsStr "Free-form extra config entries the upstream attribute set does not cover."; - - authorization = - oSub - { - policy_enforcement_mode = rStr "Policy enforcement mode ('ENFORCING', 'PERMISSIVE', or 'DISABLED')."; - decision_strategy = oStr "Decision strategy when multiple policies apply (default 'UNANIMOUS')."; - allow_remote_resource_management = oBool "Allow resource management via the protection API."; - keep_defaults = oBool "Keep default resources / scopes / permissions Keycloak creates."; - } - "Enables fine-grained authorization on the client (resource server). Required for openid_client_authorization_* resources."; - - authentication_flow_binding_overrides = oSub { - browser_id = oStr "Authentication flow id overriding the realm's browser flow for this client."; - direct_grant_id = oStr "Authentication flow id overriding the realm's direct-grant flow for this client."; - } "Per-client authentication flow overrides."; - }; - }; - - openid_client_default_scopes = { - type = "keycloak_openid_client_default_scopes"; - prefix = "openid_client_default_scopes"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = { - attr = "client_id"; - targets = [ - { - collection = "openid_clients"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed OpenID client (services.keycloak.runtime.openid_clients.) the scope binding applies to."; - }; - default_scopes = { - attr = "default_scopes"; - targets = [ - { - collection = "openid_client_scopes"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Scopes attached by default. Each entry is a managed openid_client_scope key (resolved to its name) or a literal scope name (built-ins like 'profile' / 'email' work as literals)."; - }; - }; - description = "Default OAuth2 scopes auto-attached to a client, keyed by an arbitrary label."; - attrs = { }; - }; - - openid_client_optional_scopes = { - type = "keycloak_openid_client_optional_scopes"; - prefix = "openid_client_optional_scopes"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = { - attr = "client_id"; - targets = [ - { - collection = "openid_clients"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed OpenID client (services.keycloak.runtime.openid_clients.) the scope binding applies to."; - }; - optional_scopes = { - attr = "optional_scopes"; - targets = [ - { - collection = "openid_client_scopes"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Optionally-attached scopes. Each entry is a managed openid_client_scope key (resolved to its name) or a literal scope name."; - }; - }; - description = "Optional OAuth2 scopes available to a client, keyed by an arbitrary label."; - attrs = { }; - }; - - openid_client_service_account_roles = { - type = "keycloak_openid_client_service_account_role"; - prefix = "openid_client_sa_role"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = { - attr = "client_id"; - targets = [ - { - collection = "openid_clients"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed target client whose role is granted."; - }; - }; - requiredAttrs = [ - "service_account_user_id" - "role" - ]; - description = "Grant a per-client role to a service-account user, keyed by an arbitrary label."; - attrs = { - # supply via `${keycloak_openid_client..service_account_user_id}` - service_account_user_id = oStr "Service-account user id (typically `\${keycloak_openid_client.X.service_account_user_id}`)."; - role = oStr "Name of the role granted (must exist on the target client)."; - }; - }; - - openid_client_service_account_realm_roles = { - type = "keycloak_openid_client_service_account_realm_role"; - prefix = "openid_client_sa_realm_role"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - requiredAttrs = [ - "service_account_user_id" - "role" - ]; - description = "Grant a realm-level role to a service-account user, keyed by an arbitrary label."; - attrs = { - service_account_user_id = oStr "Service-account user id (typically `\${keycloak_openid_client.X.service_account_user_id}`)."; - role = oStr "Name of the realm-level role granted."; - }; - }; - - saml_clients = { - type = "keycloak_saml_client"; - prefix = "saml_client"; - nameAttr = "client_id"; - scope = null; - refs.realm = realmRef; - # signing_private_key isn't marked Sensitive upstream but is a - # private key; expose File so it stays out of the store. - secrets = [ "signing_private_key" ]; - blockAttrs = [ "authentication_flow_binding_overrides" ]; - description = "SAML clients (per-realm), keyed by clientId."; - attrs = { - client_id = oStr "SAML clientId. Defaults to the attribute key."; - name = oStr "Display name."; - description = oStr "Client description."; - enabled = oBool "Is the client enabled?"; - - include_authn_statement = oBool "Include the AuthnStatement in assertions."; - sign_documents = oBool "Sign SAML documents."; - sign_assertions = oBool "Sign SAML assertions."; - encrypt_assertions = oBool "Encrypt assertions."; - encryption_algorithm = oStr "Assertion encryption algorithm."; - encryption_key_algorithm = oStr "Assertion encryption key algorithm."; - encryption_digest_method = oStr "Assertion encryption digest method."; - encryption_mask_generation_function = oStr "Assertion encryption MGF."; - client_signature_required = oBool "Require the client to sign requests."; - force_post_binding = oBool "Force POST binding."; - consent_required = oBool "Require consent on first use."; - front_channel_logout = oBool "Use front-channel logout."; - force_name_id_format = oBool "Force the configured name_id_format."; - signature_algorithm = oStr "SAML signature algorithm."; - signature_key_name = oStr "SAML signature key name."; - canonicalization_method = oStr "SAML canonicalization method URI."; - name_id_format = oStr "SAML NameID format."; - full_scope_allowed = oBool "Grant the full scope by default."; - - root_url = oStr "Root URL."; - valid_redirect_uris = oListStr "Valid redirect URIs."; - base_url = oStr "Base URL."; - login_theme = oStr "Per-client login theme."; - master_saml_processing_url = oStr "Master SAML processing URL."; - - encryption_certificate = oStr "Encryption certificate (PEM)."; - signing_certificate = oStr "Signing certificate (PEM)."; - signing_private_key = oStr "Signing private key (PEM). Prefer `signing_private_keyFile`."; - - idp_initiated_sso_url_name = oStr "IdP-initiated SSO URL name."; - idp_initiated_sso_relay_state = oStr "IdP-initiated SSO RelayState."; - assertion_consumer_post_url = oStr "Assertion consumer service POST URL."; - assertion_consumer_redirect_url = oStr "Assertion consumer service Redirect URL."; - logout_service_post_binding_url = oStr "SAML logout service POST binding URL."; - logout_service_redirect_binding_url = oStr "SAML logout service Redirect binding URL."; - - always_display_in_console = oBool "Always display the client in the user account console."; - extra_config = oAttrsStr "Free-form extra config entries."; - - authentication_flow_binding_overrides = oSub { - browser_id = oStr "Authentication flow id overriding the realm's browser flow for this client."; - direct_grant_id = oStr "Authentication flow id overriding the realm's direct-grant flow for this client."; - } "Per-client authentication flow overrides."; - }; - }; - - saml_client_default_scopes = { - type = "keycloak_saml_client_default_scopes"; - prefix = "saml_client_default_scopes"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = { - attr = "client_id"; - targets = [ - { - collection = "saml_clients"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed SAML client (services.keycloak.runtime.saml_clients.) the scope binding applies to."; - }; - default_scopes = { - attr = "default_scopes"; - targets = [ - { - collection = "saml_client_scopes"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "SAML scopes attached by default. Each entry is a managed saml_client_scope key (resolved to its name) or a literal scope name."; - }; - }; - description = "Default SAML scopes auto-attached to a SAML client, keyed by an arbitrary label."; - attrs = { }; - }; - - # OpenID protocol mappers: one collection per mapper type. all share - # realm + (client | client_scope) refs. - openid_user_attribute_protocol_mappers = { - type = "keycloak_openid_user_attribute_protocol_mapper"; - prefix = "openid_user_attribute_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "user_attribute" - "claim_name" - ]; - description = "OpenID protocol mapper that maps a user attribute to a claim."; - attrs = openidMapperCommonAttrs // { - multivalued = oBool "Treat the attribute as multivalued?"; - user_attribute = oStr "Name of the user attribute to map."; - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type ('String', 'long', 'int', 'boolean', 'JSON')."; - aggregate_attributes = oBool "Aggregate multiple values into one claim?"; - }; - }; - - openid_user_property_protocol_mappers = { - type = "keycloak_openid_user_property_protocol_mapper"; - prefix = "openid_user_property_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "user_property" - "claim_name" - ]; - description = "OpenID protocol mapper that maps a built-in user property (e.g. `email`, `username`) to a claim."; - attrs = openidMapperCommonAttrs // { - user_property = oStr "Built-in user property to map (e.g. 'email', 'username')."; - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type."; - }; - }; - - openid_group_membership_protocol_mappers = { - type = "keycloak_openid_group_membership_protocol_mapper"; - prefix = "openid_group_membership_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "claim_name" ]; - description = "OpenID protocol mapper that maps group memberships to a claim."; - attrs = openidMapperCommonAttrs // { - claim_name = oStr "Name of the resulting JWT claim."; - full_path = oBool "Emit full group path (/parent/child) rather than just the leaf name?"; - }; - }; - - openid_full_name_protocol_mappers = { - type = "keycloak_openid_full_name_protocol_mapper"; - prefix = "openid_full_name_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - description = "OpenID protocol mapper that emits the user's full name as a single claim."; - attrs = openidMapperCommonAttrs; - }; - - openid_sub_protocol_mappers = { - type = "keycloak_openid_sub_protocol_mapper"; - prefix = "openid_sub_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - description = "OpenID protocol mapper for the `sub` claim."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - add_to_access_token = oBool "Include in access token?"; - add_to_token_introspection = oBool "Include in token introspection?"; - }; - }; - - openid_hardcoded_claim_protocol_mappers = { - type = "keycloak_openid_hardcoded_claim_protocol_mapper"; - prefix = "openid_hardcoded_claim_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "claim_name" - "claim_value" - ]; - description = "OpenID protocol mapper that adds a hardcoded claim with a fixed value."; - attrs = openidMapperCommonAttrs // { - claim_name = oStr "Name of the resulting JWT claim."; - claim_value = oStr "Hardcoded claim value."; - claim_value_type = oStr "Claim value type."; - }; - }; - - openid_audience_protocol_mappers = { - type = "keycloak_openid_audience_protocol_mapper"; - prefix = "openid_audience_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - description = "OpenID protocol mapper that adds an audience to issued tokens (exactly one of `included_client_audience` / `included_custom_audience`)."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - included_client_audience = oStr "ClientId of a client to include as audience."; - included_custom_audience = oStr "Custom audience string to include."; - add_to_id_token = oBool "Include in ID token?"; - add_to_access_token = oBool "Include in access token?"; - }; - }; - - openid_audience_resolve_protocol_mappers = { - type = "keycloak_openid_audience_resolve_protocol_mapper"; - prefix = "openid_audience_resolve_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - description = "OpenID audience-resolve mapper (derives audience from client roles)."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - }; - }; - - openid_hardcoded_role_protocol_mappers = { - type = "keycloak_openid_hardcoded_role_protocol_mapper"; - prefix = "openid_hardcoded_role_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "role_id" ]; - description = "OpenID protocol mapper that adds a hardcoded role to issued tokens."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - role_id = oStr "Role UUID (or `\${keycloak_role.X.id}` reference) to hardcode."; - }; - }; - - openid_user_realm_role_protocol_mappers = { - type = "keycloak_openid_user_realm_role_protocol_mapper"; - prefix = "openid_user_realm_role_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "claim_name" ]; - description = "OpenID protocol mapper that maps the user's realm roles to a claim."; - attrs = openidMapperCommonAttrs // { - add_to_token_introspection = oBool "Include in token introspection?"; - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type."; - multivalued = oBool "Treat as multivalued?"; - realm_role_prefix = oStr "Optional prefix prepended to each role name in the claim."; - }; - }; - - openid_user_client_role_protocol_mappers = { - type = "keycloak_openid_user_client_role_protocol_mapper"; - prefix = "openid_user_client_role_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "claim_name" ]; - description = "OpenID protocol mapper that maps the user's roles on a specific client to a claim."; - attrs = openidMapperCommonAttrs // { - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type."; - multivalued = oBool "Treat as multivalued?"; - client_id_for_role_mappings = oStr "Source clientId whose role mappings are emitted."; - client_role_prefix = oStr "Optional prefix prepended to each role name in the claim."; - }; - }; - - openid_user_session_note_protocol_mappers = { - type = "keycloak_openid_user_session_note_protocol_mapper"; - prefix = "openid_user_session_note_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = openidClientOptionalRef; - client_scope = openidClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "claim_name" - "session_note" - ]; - description = "OpenID protocol mapper that maps a user session note to a claim."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - add_to_id_token = oBool "Include in ID token?"; - add_to_access_token = oBool "Include in access token?"; - claim_name = oStr "Name of the resulting JWT claim."; - claim_value_type = oStr "Claim value type."; - session_note = oStr "Name of the user session note to read."; - }; - }; - - saml_user_attribute_protocol_mappers = { - type = "keycloak_saml_user_attribute_protocol_mapper"; - prefix = "saml_user_attribute_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = samlClientOptionalRef; - client_scope = samlClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "user_attribute" - "saml_attribute_name" - ]; - description = "SAML mapper that exposes a user attribute as a SAML attribute."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - user_attribute = oStr "Source user attribute."; - friendly_name = oStr "Optional SAML friendlyName."; - saml_attribute_name = oStr "SAML attribute name."; - saml_attribute_name_format = oStr "SAML attribute name format ('Basic', 'URI Reference', 'Unspecified')."; - aggregate_attributes = oBool "Aggregate multivalued attributes into one SAML attribute?"; - }; - }; - - saml_user_property_protocol_mappers = { - type = "keycloak_saml_user_property_protocol_mapper"; - prefix = "saml_user_property_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = samlClientOptionalRef; - client_scope = samlClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "user_property" - "saml_attribute_name" - ]; - description = "SAML mapper that exposes a built-in user property as a SAML attribute."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - user_property = oStr "Built-in user property (e.g. 'email', 'username')."; - friendly_name = oStr "Optional SAML friendlyName."; - saml_attribute_name = oStr "SAML attribute name."; - saml_attribute_name_format = oStr "SAML attribute name format."; - }; - }; - - generic_protocol_mappers = { - type = "keycloak_generic_protocol_mapper"; - prefix = "generic_protocol_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = anyClientOptionalRef; - client_scope = anyClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "protocol" - "protocol_mapper" - "config" - ]; - description = "Generic protocol mapper escape hatch (for mappers without a dedicated typed resource)."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - protocol = oStr "Protocol ('openid-connect' or 'saml')."; - protocol_mapper = oStr "Provider-id of the mapper implementation (e.g. 'oidc-usermodel-attribute-mapper')."; - config = oAttrsStr "Mapper configuration (provider-specific key/value pairs)."; - }; - }; - - generic_client_protocol_mappers = { - type = "keycloak_generic_client_protocol_mapper"; - prefix = "generic_client_protocol_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - client = anyClientOptionalRef; - client_scope = anyClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ - "protocol" - "protocol_mapper" - "config" - ]; - description = "Generic protocol mapper attached to a specific client (without a dedicated typed resource)."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - protocol = oStr "Protocol ('openid-connect' or 'saml')."; - protocol_mapper = oStr "Provider-id of the mapper implementation."; - config = oAttrsStr "Mapper configuration (provider-specific key/value pairs)."; - }; - }; - - generic_role_mappers = { - type = "keycloak_generic_role_mapper"; - prefix = "generic_role_mapper"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = anyClientOptionalRef; - client_scope = anyClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "role_id" ]; - description = "Generic role-scope mapper that attaches a role to a client / client scope, keyed by an arbitrary label."; - attrs = { - role_id = oStr "Role UUID (or `\${keycloak_role.X.id}` reference) to attach."; - }; - }; - - generic_client_role_mappers = { - type = "keycloak_generic_client_role_mapper"; - prefix = "generic_client_role_mapper"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = anyClientOptionalRef; - client_scope = anyClientScopeOptionalRef; - }; - oneOfRefs = clientOrScopeOneOf; - requiredAttrs = [ "role_id" ]; - description = "Generic role-scope mapper attached to a specific client (deprecated alias kept for completeness)."; - attrs = { - role_id = oStr "Role UUID (or `\${keycloak_role.X.id}` reference) to attach."; - }; - }; - - oidc_identity_providers = { - type = "keycloak_oidc_identity_provider"; - prefix = "oidc_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - secrets = [ "client_secret" ]; - requiredAttrs = [ - "authorization_url" - "client_id" - "token_url" - ]; - description = "Generic OIDC identity providers (per-realm), keyed by alias."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'oidc')."; - backchannel_supported = oBool "Does the IdP support back-channel logout?"; - validate_signature = oBool "Validate the IdP's token signature."; - authorization_url = oStr "OIDC authorization endpoint."; - client_id = oStr "OIDC client id."; - client_secret = oStr "OIDC client secret. Prefer `client_secretFile`."; - user_info_url = oStr "OIDC userinfo endpoint."; - jwks_url = oStr "OIDC JWKS endpoint."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - token_url = oStr "OIDC token endpoint."; - logout_url = oStr "OIDC logout endpoint."; - login_hint = oBool "Pass `login_hint` query parameter to the IdP."; - ui_locales = oBool "Pass `ui_locales` query parameter to the IdP."; - default_scopes = oStr "Space-separated default scopes to request."; - accepts_prompt_none_forward_from_client = oBool "Forward `prompt=none` requests to this IdP."; - disable_user_info = oBool "Don't call the userinfo endpoint."; - issuer = oStr "Expected `iss` claim value."; - disable_type_claim_check = oBool "Skip the typ-claim check on returned tokens."; - }; - }; - - saml_identity_providers = { - type = "keycloak_saml_identity_provider"; - prefix = "saml_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - requiredAttrs = [ - "entity_id" - "single_sign_on_service_url" - ]; - description = "SAML identity providers (per-realm), keyed by alias."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'saml')."; - backchannel_supported = oBool "Does the IdP support back-channel logout?"; - validate_signature = oBool "Validate SAML signatures."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - name_id_policy_format = oStr "Default name_id_policy_format URN."; - single_logout_service_url = oStr "SAML SLO endpoint URL."; - entity_id = oStr "Entity ID expected from the IdP."; - single_sign_on_service_url = oStr "SAML SSO endpoint URL."; - signing_certificate = oStr "IdP signing certificate (PEM)."; - signature_algorithm = oStr "Signature algorithm."; - xml_sign_key_info_key_name_transformer = oStr "KeyInfo KeyName transformer."; - post_binding_authn_request = oBool "Use POST binding for AuthnRequests."; - post_binding_response = oBool "Use POST binding for Responses."; - post_binding_logout = oBool "Use POST binding for Logout."; - force_authn = oBool "Force re-authentication on every login."; - login_hint = oBool "Pass login_hint to the IdP."; - want_assertions_signed = oBool "Require signed assertions."; - want_assertions_encrypted = oBool "Require encrypted assertions."; - want_authn_requests_signed = oBool "Require signed AuthnRequests."; - principal_type = oStr "How to derive the user principal ('SUBJECT', 'ATTRIBUTE', 'FRIENDLY_ATTRIBUTE')."; - principal_attribute = oStr "Attribute name when principal_type is ATTRIBUTE or FRIENDLY_ATTRIBUTE."; - authn_context_class_refs = oListStr "AuthnContext class refs requested in AuthnRequests."; - authn_context_decl_refs = oListStr "AuthnContext declaration refs requested in AuthnRequests."; - authn_context_comparison_type = oStr "AuthnContext comparison type ('exact', 'minimum', 'maximum', 'better')."; - }; - }; - - oidc_google_identity_providers = { - type = "keycloak_oidc_google_identity_provider"; - prefix = "oidc_google_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - secrets = [ "client_secret" ]; - requiredSecrets = [ "client_secret" ]; - requiredAttrs = [ "client_id" ]; - description = "Google OIDC identity providers (per-realm), keyed by alias (defaults to 'google')."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'google')."; - client_id = oStr "Google OAuth2 client id."; - client_secret = oStr "Google OAuth2 client secret. Prefer `client_secretFile`."; - hosted_domain = oStr "Restrict to a Google Workspace hosted domain (or `*`)."; - use_user_ip_param = oBool "Forward the user's IP to Google's UserInfo service."; - request_refresh_token = oBool "Request a refresh token (`access_type=offline`)."; - default_scopes = oStr "Space-separated default scopes (default 'openid profile email')."; - accepts_prompt_none_forward_from_client = oBool "Forward `prompt=none` requests."; - disable_user_info = oBool "Don't call the UserInfo service."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - }; - }; - - oidc_facebook_identity_providers = { - type = "keycloak_oidc_facebook_identity_provider"; - prefix = "oidc_facebook_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - secrets = [ "client_secret" ]; - requiredSecrets = [ "client_secret" ]; - requiredAttrs = [ "client_id" ]; - description = "Facebook OIDC identity providers (per-realm), keyed by alias (defaults to 'facebook')."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'facebook')."; - client_id = oStr "Facebook app id."; - client_secret = oStr "Facebook app secret. Prefer `client_secretFile`."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - }; - }; - - oidc_github_identity_providers = { - type = "keycloak_oidc_github_identity_provider"; - prefix = "oidc_github_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - secrets = [ "client_secret" ]; - requiredSecrets = [ "client_secret" ]; - requiredAttrs = [ "client_id" ]; - description = "GitHub OIDC identity providers (per-realm), keyed by alias (defaults to 'github')."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'github')."; - client_id = oStr "GitHub OAuth app client id."; - client_secret = oStr "GitHub OAuth app client secret. Prefer `client_secretFile`."; - base_url = oStr "Override the GitHub Enterprise base URL."; - api_url = oStr "Override the GitHub Enterprise API URL."; - github_json_format = oBool "Use GitHub's JSON content type."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - }; - }; - - kubernetes_identity_providers = { - type = "keycloak_kubernetes_identity_provider"; - prefix = "kubernetes_idp"; - nameAttr = "alias"; - scope = null; - refs.realm = realmAliasRef; - requiredAttrs = [ "issuer" ]; - description = "Kubernetes OIDC identity providers (per-realm), keyed by alias."; - attrs = commonIdpAttrs // { - provider_id = oStr "Provider id (defaults to 'kubernetes')."; - issuer = oStr "Kubernetes API server issuer URL."; - hide_on_login_page = oBool "Hide this IdP on the login page."; - }; - }; - - hardcoded_attribute_identity_provider_mappers = { - type = "keycloak_hardcoded_attribute_identity_provider_mapper"; - prefix = "hardcoded_attribute_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - requiredAttrs = [ "user_session" ]; - description = "Sets a hardcoded user (or session-note) attribute on every federated user."; - attrs = commonIdpMapperAttrs // { - attribute_name = oStr "Name of the attribute / session note to set."; - attribute_value = oStr "Value of the attribute / session note."; - user_session = oBool "If true, set as a session note; if false, as a user attribute."; - }; - }; - - hardcoded_group_identity_provider_mappers = { - type = "keycloak_hardcoded_group_identity_provider_mapper"; - prefix = "hardcoded_group_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - description = "Adds every federated user to a hardcoded group."; - attrs = commonIdpMapperAttrs // { - group = oStr "Group path (e.g. `/engineering/backend`) every federated user joins."; - }; - }; - - hardcoded_role_identity_provider_mappers = { - type = "keycloak_hardcoded_role_identity_provider_mapper"; - prefix = "hardcoded_role_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - description = "Grants a hardcoded role to every federated user."; - attrs = commonIdpMapperAttrs // { - role = oStr "Realm or `client.role` name granted to every federated user."; - }; - }; - - attribute_importer_identity_provider_mappers = { - type = "keycloak_attribute_importer_identity_provider_mapper"; - prefix = "attribute_importer_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - requiredAttrs = [ "user_attribute" ]; - description = "Imports an attribute / claim from the IdP onto the federated user."; - attrs = commonIdpMapperAttrs // { - user_attribute = oStr "Destination user attribute on the keycloak side."; - attribute_name = oStr "Source SAML attribute name (SAML IdPs; conflicts with attribute_friendly_name)."; - attribute_friendly_name = oStr "Source SAML attribute friendly name (SAML IdPs; conflicts with attribute_name)."; - claim_name = oStr "Source OIDC claim name (OIDC IdPs)."; - }; - }; - - attribute_to_role_identity_provider_mappers = { - type = "keycloak_attribute_to_role_identity_provider_mapper"; - prefix = "attribute_to_role_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - requiredAttrs = [ "role" ]; - description = "Grants a role to federated users whose IdP attribute / claim matches a value."; - attrs = commonIdpMapperAttrs // { - attribute_name = oStr "SAML attribute name to match (conflicts with attribute_friendly_name)."; - attribute_value = oStr "Value the SAML attribute must equal."; - attribute_friendly_name = oStr "SAML friendly name to match (conflicts with attribute_name)."; - claim_name = oStr "OIDC claim name to match."; - claim_value = oStr "Value the OIDC claim must equal."; - role = oStr "Realm or `client.role` name granted on match."; - }; - }; - - user_template_importer_identity_provider_mappers = { - type = "keycloak_user_template_importer_identity_provider_mapper"; - prefix = "user_template_importer_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - description = "Derives the federated user's username from a Mustache-style template over IdP claims."; - attrs = commonIdpMapperAttrs // { - template = oStr "Username template (e.g. `\${CLAIM.preferred_username}@example`)."; - }; - }; - - custom_identity_provider_mappers = { - type = "keycloak_custom_identity_provider_mapper"; - prefix = "custom_idp_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmAliasRef; - identity_provider = idpAliasRequiredRef; - }; - requiredAttrs = [ "identity_provider_mapper" ]; - description = "Escape hatch for an IdP mapper implementation without a dedicated typed resource."; - attrs = commonIdpMapperAttrs // { - identity_provider_mapper = oStr "Provider-id of the mapper implementation."; - }; - }; - - authentication_flows = { - type = "keycloak_authentication_flow"; - prefix = "authentication_flow"; - nameAttr = "alias"; - scope = null; - refs.realm = realmRef; - description = "Top-level authentication flows (per-realm), keyed by alias."; - attrs = { - alias = oStr "Flow alias. Defaults to the attribute key."; - provider_id = oStr "Flow implementation: 'basic-flow' (default) or 'client-flow'."; - description = oStr "Flow description."; - }; - }; - - authentication_subflows = { - type = "keycloak_authentication_subflow"; - prefix = "authentication_subflow"; - nameAttr = "alias"; - scope = null; - refs = { - realm = realmRef; - parent_flow = { - attr = "parent_flow_alias"; - targets = [ - { - collection = "authentication_flows"; - field = "alias"; - } - { - collection = "authentication_subflows"; - field = "alias"; - } - ]; - managedOnly = false; - required = true; - description = "Alias of the parent flow (managed key or literal alias)."; - }; - }; - description = "Authentication subflows nested under a parent flow, keyed by alias."; - attrs = { - alias = oStr "Subflow alias. Defaults to the attribute key."; - provider_id = oStr "Subflow implementation: 'basic-flow' (default), 'form-flow', or 'client-flow'."; - description = oStr "Subflow description."; - authenticator = oStr "Authenticator id (for form / conditional subflows)."; - requirement = oStr "Execution requirement ('REQUIRED', 'ALTERNATIVE', 'OPTIONAL', 'CONDITIONAL', 'DISABLED')."; - priority = oInt "Display / evaluation order within the parent flow."; - }; - }; - - authentication_executions = { - type = "keycloak_authentication_execution"; - prefix = "authentication_execution"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - parent_flow = { - attr = "parent_flow_alias"; - targets = [ - { - collection = "authentication_flows"; - field = "alias"; - } - { - collection = "authentication_subflows"; - field = "alias"; - } - ]; - managedOnly = false; - required = true; - description = "Alias of the parent flow / subflow (managed key or literal alias)."; - }; - }; - requiredAttrs = [ "authenticator" ]; - description = "Authentication executions inside a flow / subflow, keyed by an arbitrary label."; - attrs = { - authenticator = oStr "Authenticator provider id (e.g. 'auth-username-password-form')."; - requirement = oStr "Execution requirement ('REQUIRED', 'ALTERNATIVE', 'OPTIONAL', 'CONDITIONAL', 'DISABLED')."; - priority = oInt "Display / evaluation order within the parent flow."; - }; - }; - - authentication_execution_configs = { - type = "keycloak_authentication_execution_config"; - prefix = "authentication_execution_config"; - nameAttr = "alias"; - scope = null; - refs = { - realm = realmRef; - execution = { - attr = "execution_id"; - targets = [ - { - collection = "authentication_executions"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed authentication_execution (services.keycloak.runtime.authentication_executions.) this config attaches to."; - }; - }; - requiredAttrs = [ "config" ]; - description = "Per-execution configuration map, keyed by config alias."; - attrs = { - alias = oStr "Config alias. Defaults to the attribute key."; - config = oAttrsStr "Execution config key/value pairs."; - }; - }; - - authentication_bindings = { - type = "keycloak_authentication_bindings"; - prefix = "authentication_bindings"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - description = "Realm-level authentication flow bindings (browser / registration / direct grant / etc.), keyed by an arbitrary label."; - attrs = { - browser_flow = oStr "Alias of the flow bound to the browser flow."; - registration_flow = oStr "Alias of the flow bound to the registration flow."; - direct_grant_flow = oStr "Alias of the flow bound to the direct-grant flow."; - reset_credentials_flow = oStr "Alias of the flow bound to the reset-credentials flow."; - client_authentication_flow = oStr "Alias of the flow bound to the client-auth flow."; - docker_authentication_flow = oStr "Alias of the flow bound to the docker-auth flow."; - first_broker_login_flow = oStr "Alias of the flow bound to the first-broker-login flow."; - }; - }; - - openid_client_authorization_resources = { - type = "keycloak_openid_client_authorization_resource"; - prefix = "openid_client_authz_resource"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client (with authorization enabled) hosting this resource."; - }; - }; - description = "Authorization resources hosted on an openid_client's resource server."; - attrs = { - name = oStr "Resource name. Defaults to the attribute key."; - display_name = oStr "Human-friendly display name."; - uris = oListStr "URIs the resource represents."; - icon_uri = oStr "Optional icon URI."; - owner_managed_access = oBool "Allow the owner to manage access to this resource."; - scopes = oListStr "Names of authorization scopes available on the resource."; - type = oStr "Optional resource type discriminator."; - attributes = oAttrsStr "Free-form attribute map."; - }; - }; - - openid_client_authorization_scopes = { - type = "keycloak_openid_client_authorization_scope"; - prefix = "openid_client_authz_scope"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client (with authorization enabled) hosting this scope."; - }; - }; - description = "Authorization scopes on an openid_client's resource server."; - attrs = { - name = oStr "Scope name. Defaults to the attribute key."; - display_name = oStr "Human-friendly display name."; - icon_uri = oStr "Optional icon URI."; - }; - }; - - openid_client_authorization_permissions = { - type = "keycloak_openid_client_authorization_permission"; - prefix = "openid_client_authz_permission"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client (with authorization enabled) hosting this permission."; - }; - }; - description = "Authorization permissions tying resources/scopes to policies."; - attrs = { - name = oStr "Permission name. Defaults to the attribute key."; - description = oStr "Permission description."; - decision_strategy = oStr "Decision strategy ('UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS'; default 'UNANIMOUS')."; - policies = oListStr "Names / ids of policies that apply."; - resources = oListStr "Resource names this permission covers (conflicts with resource_type)."; - resource_type = oStr "Single resource type this permission covers (conflicts with resources)."; - scopes = oListStr "Scope names this permission covers."; - type = oStr "Permission type ('resource' [default] or 'scope')."; - }; - }; - - openid_client_aggregate_policies = { - type = "keycloak_openid_client_aggregate_policy"; - prefix = "openid_client_aggregate_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this aggregate policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "policies" - ]; - description = "Aggregate policy combining other policies under a decision strategy."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy ('UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS')."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - policies = oListStr "Names / ids of policies aggregated by this policy."; - }; - }; - - openid_client_client_policies = { - type = "keycloak_openid_client_client_policy"; - prefix = "openid_client_client_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "clients" - ]; - description = "Policy granting access to a specific set of clients."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - clients = oListStr "ClientIds of clients the policy applies to."; - }; - }; - - openid_client_authorization_client_scope_policies = { - type = "keycloak_openid_client_authorization_client_scope_policy"; - prefix = "openid_client_authz_client_scope_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "scope" - ]; - description = "Policy granting access by client scope membership; each scope block is `{ id; required = false; }`."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - scope = oListSub { - id = rStr "Client scope id."; - required = oBool "Treat the scope as required (vs optional)."; - } "List of `{ id; required; }` blocks naming client scopes the policy applies to."; - }; - }; - - openid_client_group_policies = { - type = "keycloak_openid_client_group_policy"; - prefix = "openid_client_group_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "groups" - ]; - description = "Policy granting access by group membership; each group block is `{ id; path; extend_children; }`."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - groups_claim = oStr "Optional JWT claim whose value carries the group path."; - groups = oListSub { - id = rStr "Group id."; - path = oStr "Group path (read from the API)."; - extend_children = oBool "Match descendants of the group as well."; - } "List of `{ id; path; extend_children; }` blocks naming groups the policy applies to."; - }; - }; - - openid_client_role_policies = { - type = "keycloak_openid_client_role_policy"; - prefix = "openid_client_role_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "role" - ]; - description = "Policy granting access by realm or client role membership; each role block is `{ id; required = false; }`."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - type = oStr "Policy type discriminator."; - fetch_roles = oBool "Fetch role information on policy evaluation."; - role = oListSub { - id = rStr "Role id."; - required = oBool "Treat the role as required (vs optional)."; - } "List of `{ id; required; }` blocks naming roles the policy applies to."; - }; - }; - - openid_client_time_policies = { - type = "keycloak_openid_client_time_policy"; - prefix = "openid_client_time_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ "decision_strategy" ]; - description = "Policy granting access within a time window."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - not_before = oStr "Date-time before which access is denied (`YYYY-MM-DD HH:MM:SS`)."; - not_on_or_after = oStr "Date-time on or after which access is denied."; - day_month = oStr "Day-of-month window start."; - day_month_end = oStr "Day-of-month window end."; - month = oStr "Month window start."; - month_end = oStr "Month window end."; - year = oStr "Year window start."; - year_end = oStr "Year window end."; - hour = oStr "Hour-of-day window start."; - hour_end = oStr "Hour-of-day window end."; - minute = oStr "Minute-of-hour window start."; - minute_end = oStr "Minute-of-hour window end."; - }; - }; - - ldap_user_federations = { - type = "keycloak_ldap_user_federation"; - prefix = "ldap_user_federation"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - secrets = [ "bind_credential" ]; - requiredAttrs = [ - "username_ldap_attribute" - "rdn_ldap_attribute" - "uuid_ldap_attribute" - "user_object_classes" - "connection_url" - "users_dn" - ]; - blockAttrs = [ - "kerberos" - "cache" - ]; - description = "LDAP user federations (per-realm), keyed by name."; - attrs = { - name = oStr "Federation name. Defaults to the attribute key."; - enabled = oBool "Is the federation enabled?"; - priority = oInt "Evaluation priority (lower runs first)."; - import_enabled = oBool "Import users from LDAP into Keycloak's local DB."; - edit_mode = oStr "'READ_ONLY' (default), 'WRITABLE', or 'UNSYNCED'."; - sync_registrations = oBool "Write new user registrations back into LDAP."; - vendor = oStr "LDAP vendor: 'OTHER' (default), 'EDIRECTORY', 'AD', 'RHDS', 'TIVOLI'."; - username_ldap_attribute = oStr "LDAP attribute carrying the username."; - rdn_ldap_attribute = oStr "LDAP RDN attribute."; - uuid_ldap_attribute = oStr "LDAP attribute carrying a stable UUID."; - user_object_classes = oListStr "LDAP objectClasses for users."; - connection_url = oStr "LDAP connection URL (ldap[s]://host:port)."; - users_dn = oStr "Base DN under which users live."; - bind_dn = oStr "DN used to authenticate to LDAP (omit for anonymous bind)."; - bind_credential = oStr "Password for bind_dn. Prefer `bind_credentialFile`."; - custom_user_search_filter = oStr "Extra LDAP filter applied when looking up users."; - krb_principal_attribute = oStr "LDAP attribute carrying the Kerberos principal."; - debug = oStr "Enable LDAP debug logging ('true' / 'false')."; - search_scope = oStr "Search scope: 'ONE_LEVEL' (default) or 'SUBTREE'."; - start_tls = oBool "Issue STARTTLS after connecting."; - connection_pooling = oBool "Pool LDAP connections."; - use_password_modify_extended_op = oBool "Use the LDAP password modify extended operation."; - validate_password_policy = oBool "Validate passwords against the realm's password policy."; - trust_email = oBool "Trust the email returned by LDAP without verification."; - use_truststore_spi = oStr "Truststore SPI usage: 'ALWAYS', 'ONLY_FOR_LDAPS' (default), or 'NEVER'."; - connection_timeout = oStr "LDAP connection timeout (duration string)."; - read_timeout = oStr "LDAP read timeout (duration string)."; - pagination = oBool "Enable LDAP pagination."; - batch_size_for_sync = oInt "Number of users per sync batch."; - full_sync_period = oInt "Full sync period in seconds (-1 disables)."; - changed_sync_period = oInt "Incremental sync period in seconds (-1 disables)."; - delete_default_mappers = oBool "Remove the default protocol mappers shipped with the federation."; - kerberos = oSub { - kerberos_realm = oStr "Kerberos realm."; - server_principal = oStr "Kerberos service principal of the LDAP server."; - key_tab = oStr "Path to the keytab file."; - use_kerberos_for_password_authentication = oBool "Use Kerberos for password auth."; - } "Kerberos integration."; - cache = oSub { - policy = oStr "Cache policy ('DEFAULT', 'EVICT_DAILY', 'EVICT_WEEKLY', 'MAX_LIFESPAN', 'NO_CACHE')."; - max_lifespan = oStr "Max lifespan (for MAX_LIFESPAN)."; - eviction_day = oStr "Eviction day (for EVICT_WEEKLY)."; - eviction_hour = oStr "Eviction hour."; - eviction_minute = oStr "Eviction minute."; - } "Cache configuration."; - }; - }; - - ldap_user_attribute_mappers = { - type = "keycloak_ldap_user_attribute_mapper"; - prefix = "ldap_user_attribute_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "user_model_attribute" - "ldap_attribute" - ]; - description = "Maps a keycloak user attribute to an LDAP attribute."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - user_model_attribute = oStr "Keycloak-side user attribute name."; - ldap_attribute = oStr "LDAP attribute name."; - read_only = oBool "Treat LDAP as the source of truth (writes are no-ops)."; - always_read_value_from_ldap = oBool "Re-read value from LDAP on every access."; - is_mandatory_in_ldap = oBool "LDAP enforces presence of the attribute."; - attribute_force_default = oBool "Force the default value when the LDAP attribute is missing."; - attribute_default_value = oStr "Default value used when LDAP returns none."; - is_binary_attribute = oBool "Treat the LDAP attribute as binary."; - }; - }; - - ldap_group_mappers = { - type = "keycloak_ldap_group_mapper"; - prefix = "ldap_group_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "ldap_groups_dn" - "group_name_ldap_attribute" - "group_object_classes" - "membership_ldap_attribute" - "membership_user_ldap_attribute" - ]; - description = "Maps LDAP groups onto keycloak groups."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - ldap_groups_dn = oStr "Base DN under which groups live."; - group_name_ldap_attribute = oStr "LDAP attribute carrying the group name."; - group_object_classes = oListStr "LDAP objectClasses for groups."; - preserve_group_inheritance = oBool "Preserve nested group hierarchy."; - ignore_missing_groups = oBool "Ignore membership entries pointing to missing groups."; - membership_ldap_attribute = oStr "LDAP attribute on the group holding member references."; - membership_attribute_type = oStr "'DN' (default) or 'UID'."; - membership_user_ldap_attribute = oStr "LDAP attribute on the user that uniquely identifies them."; - groups_ldap_filter = oStr "Extra LDAP filter for group lookups."; - mode = oStr "Mapper mode: 'READ_ONLY' (default), 'LDAP_ONLY', or 'IMPORT'."; - user_roles_retrieve_strategy = oStr "Strategy for resolving a user's groups."; - memberof_ldap_attribute = oStr "LDAP attribute holding direct group memberships (memberOf-style)."; - mapped_group_attributes = oListStr "LDAP group attributes preserved into keycloak."; - drop_non_existing_groups_during_sync = oBool "Delete keycloak groups missing from LDAP during sync."; - groups_path = oStr "Path under which mapped groups live (e.g. `/ldap-groups`)."; - }; - }; - - ldap_role_mappers = { - type = "keycloak_ldap_role_mapper"; - prefix = "ldap_role_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "ldap_roles_dn" - "role_name_ldap_attribute" - "role_object_classes" - "membership_ldap_attribute" - "membership_user_ldap_attribute" - ]; - description = "Maps LDAP roles onto keycloak realm or client roles."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - ldap_roles_dn = oStr "Base DN under which roles live."; - role_name_ldap_attribute = oStr "LDAP attribute carrying the role name."; - role_object_classes = oListStr "LDAP objectClasses for roles."; - membership_ldap_attribute = oStr "LDAP attribute on the role holding member references."; - membership_attribute_type = oStr "'DN' (default) or 'UID'."; - membership_user_ldap_attribute = oStr "LDAP attribute on the user that uniquely identifies them."; - roles_ldap_filter = oStr "Extra LDAP filter for role lookups."; - mode = oStr "Mapper mode: 'READ_ONLY' (default), 'LDAP_ONLY', or 'IMPORT'."; - user_roles_retrieve_strategy = oStr "Strategy for resolving a user's roles."; - memberof_ldap_attribute = oStr "LDAP attribute holding direct role memberships."; - use_realm_roles_mapping = oBool "Map onto realm roles (true) or client roles (false)."; - client_id = oStr "ClientId roles are scoped to when `use_realm_roles_mapping = false`."; - }; - }; - - ldap_hardcoded_role_mappers = { - type = "keycloak_ldap_hardcoded_role_mapper"; - prefix = "ldap_hardcoded_role_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ "role" ]; - description = "Grants a hardcoded role to every LDAP-federated user."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - role = oStr "Realm or `client.role` name granted."; - }; - }; - - ldap_hardcoded_attribute_mappers = { - type = "keycloak_ldap_hardcoded_attribute_mapper"; - prefix = "ldap_hardcoded_attribute_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "attribute_name" - "attribute_value" - ]; - description = "Sets a hardcoded user attribute on every LDAP-federated user."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - attribute_name = oStr "Name of the attribute to set."; - attribute_value = oStr "Value of the attribute."; - }; - }; - - ldap_hardcoded_group_mappers = { - type = "keycloak_ldap_hardcoded_group_mapper"; - prefix = "ldap_hardcoded_group_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ "group" ]; - description = "Adds every LDAP-federated user to a hardcoded group."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - group = oStr "Group path (e.g. `/engineering`) every federated user joins."; - }; - }; - - ldap_msad_user_account_control_mappers = { - type = "keycloak_ldap_msad_user_account_control_mapper"; - prefix = "ldap_msad_uac_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - description = "MSAD userAccountControl integration mapper (enables / disables and locks out users)."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - ldap_password_policy_hints_enabled = oBool "Forward keycloak password-policy hints to MSAD."; - }; - }; - - ldap_msad_lds_user_account_control_mappers = { - type = "keycloak_ldap_msad_lds_user_account_control_mapper"; - prefix = "ldap_msad_lds_uac_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - description = "MSAD LDS userAccountControl integration mapper."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - }; - }; - - ldap_full_name_mappers = { - type = "keycloak_ldap_full_name_mapper"; - prefix = "ldap_full_name_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ "ldap_full_name_attribute" ]; - description = "Splits/joins a single LDAP full-name attribute into keycloak's first / last name fields."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - ldap_full_name_attribute = oStr "LDAP attribute carrying the full name."; - read_only = oBool "Treat LDAP as source of truth."; - write_only = oBool "Only push the full name back to LDAP."; - }; - }; - - ldap_custom_mappers = { - type = "keycloak_ldap_custom_mapper"; - prefix = "ldap_custom_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "provider_id" - "provider_type" - ]; - description = "Escape hatch for an LDAP mapper implementation without a dedicated typed resource."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - provider_id = oStr "Provider-id of the mapper implementation."; - provider_type = oStr "SPI type the provider implements."; - config = oAttrsStr "Mapper-specific configuration."; - }; - }; - - custom_user_federations = { - type = "keycloak_custom_user_federation"; - prefix = "custom_user_federation"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - requiredAttrs = [ "provider_id" ]; - description = "Custom user federation backed by a JPA / SPI provider."; - attrs = { - name = oStr "Federation name. Defaults to the attribute key."; - parent_id = oStr "Optional parent federation id."; - provider_id = oStr "Provider-id of the federation implementation."; - enabled = oBool "Is the federation enabled?"; - priority = oInt "Evaluation priority (lower runs first)."; - cache_policy = oStr "Cache policy: 'DEFAULT', 'EVICT_DAILY', 'EVICT_WEEKLY', 'MAX_LIFESPAN', 'NO_CACHE'."; - full_sync_period = oInt "Full sync period in seconds (-1 disables)."; - changed_sync_period = oInt "Incremental sync period in seconds (-1 disables)."; - config = oAttrsStr "Provider-specific configuration map."; - }; - }; - - realm_keystore_aes_generateds = { - type = "keycloak_realm_keystore_aes_generated"; - prefix = "realm_keystore_aes_generated"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "AES keystore generated by Keycloak."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - secret_size = oInt "Secret size in bytes (16, 24, or 32; default 16)."; - }; - }; - - realm_keystore_ecdsa_generateds = { - type = "keycloak_realm_keystore_ecdsa_generated"; - prefix = "realm_keystore_ecdsa_generated"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "ECDSA keystore generated by Keycloak."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - elliptic_curve_key = oStr "Curve: 'P-256' (default), 'P-384', or 'P-521'."; - }; - }; - - realm_keystore_hmac_generateds = { - type = "keycloak_realm_keystore_hmac_generated"; - prefix = "realm_keystore_hmac_generated"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "HMAC keystore generated by Keycloak."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - algorithm = oStr "HMAC algorithm: 'HS256' (default), 'HS384', or 'HS512'."; - secret_size = oInt "Secret size in bytes (16, 24, 32, 64, 128, 256, or 512; default 64)."; - }; - }; - - realm_keystore_java_keystores = { - type = "keycloak_realm_keystore_java_keystore"; - prefix = "realm_keystore_java_keystore"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - secrets = [ - "keystore_password" - "key_password" - ]; - requiredSecrets = [ - "keystore_password" - "key_password" - ]; - requiredAttrs = [ - "keystore" - "key_alias" - ]; - description = "Keystore backed by a Java KeyStore (JKS) file."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - algorithm = oStr "Signing algorithm (default 'RS256')."; - keystore = oStr "Host path to the JKS file (on the keycloak server)."; - keystore_password = oStr "Password unlocking the JKS file. Prefer `keystore_passwordFile`."; - key_alias = oStr "Key alias within the JKS file."; - key_password = oStr "Password unlocking the key entry. Prefer `key_passwordFile`."; - }; - }; - - realm_keystore_rsas = { - type = "keycloak_realm_keystore_rsa"; - prefix = "realm_keystore_rsa"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - # private_key and certificate are PEM material; expose File - # for both even though only private_key is technically secret. - secrets = [ - "private_key" - "certificate" - ]; - requiredSecrets = [ - "private_key" - "certificate" - ]; - description = "Keystore backed by an externally-provided RSA private key / certificate pair."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - algorithm = oStr "Signing algorithm (default 'RS256')."; - private_key = oStr "PEM-encoded RSA private key. Prefer `private_keyFile`."; - certificate = oStr "PEM-encoded certificate. Prefer `certificateFile`."; - provider_id = oStr "Provider id (default 'rsa')."; - extra_config = oAttrsStr "Free-form extra config entries."; - }; - }; - - realm_keystore_rsa_generateds = { - type = "keycloak_realm_keystore_rsa_generated"; - prefix = "realm_keystore_rsa_generated"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "RSA keystore generated by Keycloak."; - attrs = { - name = oStr "Keystore name. Defaults to the attribute key."; - active = oBool "Is the key active?"; - enabled = oBool "Is the keystore enabled?"; - priority = oInt "Selection priority."; - algorithm = oStr "Signing algorithm: 'RS256' (default), 'RS384', 'RS512', 'PS256', 'PS384', or 'PS512'."; - key_size = oInt "Key size in bits (1024, 2048, or 4096; default 2048)."; - }; - }; - - hardcoded_attribute_mappers = { - type = "keycloak_hardcoded_attribute_mapper"; - prefix = "hardcoded_attribute_mapper"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - ldap_user_federation = ldapFederationIdRef; - }; - requiredAttrs = [ - "attribute_name" - "attribute_value" - ]; - description = "Sets a hardcoded user attribute on every federated user. Distinct from ldap_hardcoded_attribute_mapper and hardcoded_attribute_identity_provider_mapper."; - attrs = { - name = oStr "Mapper name. Defaults to the attribute key."; - attribute_name = oStr "Name of the attribute to set."; - attribute_value = oStr "Value of the attribute."; - }; - }; - - openid_client_user_policies = { - type = "keycloak_openid_client_user_policy"; - prefix = "openid_client_user_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - resource_server = { - attr = "resource_server_id"; - targets = [ - { - collection = "openid_clients"; - field = "resource_server_id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client hosting this policy."; - }; - }; - requiredAttrs = [ - "decision_strategy" - "users" - ]; - description = "Policy granting access to a specific set of users."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - decision_strategy = oStr "Decision strategy."; - logic = oStr "Policy logic ('POSITIVE' or 'NEGATIVE')."; - users = oListStr "User ids the policy applies to."; - }; - }; - - required_actions = { - type = "keycloak_required_action"; - prefix = "required_action"; - nameAttr = "alias"; - scope = null; - refs.realm = realmRef; - description = "Realm required actions (per-realm), keyed by alias."; - attrs = { - alias = oStr "Required action alias (e.g. 'CONFIGURE_TOTP'). Defaults to the attribute key."; - name = oStr "Display name shown to the user."; - enabled = oBool "Is the required action enabled?"; - default_action = oBool "Is the action set as a default for new users?"; - priority = oInt "Display / evaluation order."; - config = oAttrsStr "Action-specific configuration."; - }; - }; - - realm_events = { - type = "keycloak_realm_events"; - prefix = "realm_events"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - description = "Per-realm event logging configuration, keyed by an arbitrary label."; - attrs = { - admin_events_details_enabled = oBool "Log admin event representation details."; - admin_events_enabled = oBool "Log admin events."; - enabled_event_types = oListStr "Event types to log (empty list = all)."; - events_enabled = oBool "Log user events."; - events_expiration = oInt "User-event retention period in seconds (0 = forever)."; - events_listeners = oListStr "SPI listeners receiving events (e.g. [\"jboss-logging\"])."; - }; - }; - - realm_localizations = { - type = "keycloak_realm_localization"; - prefix = "realm_localization"; - nameAttr = "locale"; - scope = null; - refs.realm = realmRef; - description = "Per-realm i18n message bundle, keyed by locale."; - attrs = { - locale = oStr "BCP-47 locale tag (e.g. 'en'). Defaults to the attribute key."; - texts = oAttrsStr "Message-key to translation map."; - }; - }; - - realm_default_client_scopes = { - type = "keycloak_realm_default_client_scopes"; - prefix = "realm_default_client_scopes"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - default_scopes = { - attr = "default_scopes"; - targets = [ - { - collection = "openid_client_scopes"; - field = "name"; - } - { - collection = "saml_client_scopes"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Scope names auto-attached as default to every new client. Each entry is a managed openid/saml client_scope key (resolved to its name) or a literal scope name."; - }; - }; - description = "Realm-wide default client-scope binding (set of scope names), keyed by an arbitrary label. Distinct from realms..default_default_client_scopes, which is a free-form realm attribute."; - attrs = { }; - }; - - realm_optional_client_scopes = { - type = "keycloak_realm_optional_client_scopes"; - prefix = "realm_optional_client_scopes"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - optional_scopes = { - attr = "optional_scopes"; - targets = [ - { - collection = "openid_client_scopes"; - field = "name"; - } - { - collection = "saml_client_scopes"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Scope names available as optional to every new client. Each entry is a managed openid/saml client_scope key (resolved to its name) or a literal scope name."; - }; - }; - description = "Realm-wide optional client-scope binding (set of scope names), keyed by an arbitrary label."; - attrs = { }; - }; - - organizations = { - type = "keycloak_organization"; - prefix = "organization"; - nameAttr = "name"; - scope = null; - refs.realm = realmAliasRef; - description = "Keycloak organizations (per-realm, requires the organizations feature), keyed by name."; - attrs = { - name = oStr "Organization name. Defaults to the attribute key."; - alias = oStr "Stable alias (defaults to a normalised form of the name)."; - enabled = oBool "Is the organization enabled?"; - description = oStr "Organization description."; - redirect_url = oStr "Optional redirect URL for organization-aware flows."; - # domain is a list of nested blocks; renders as a json array, - # no blockAttrs wrap needed. - domain = oListSub { - name = rStr "Domain name (e.g. acme.example)."; - verified = oBool "Has the domain been verified?"; - } "List of `{ name; verified; }` domains owned by the organization."; - attributes = oAttrsStr "Free-form organization attribute map."; - }; - }; - - identity_provider_token_exchange_scope_permissions = { - type = "keycloak_identity_provider_token_exchange_scope_permission"; - prefix = "idp_token_exchange_perm"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - requiredAttrs = [ - "provider_alias" - "clients" - ]; - description = "Per-IdP token-exchange permission policy granting a set of clients access to the IdP's token-exchange scope."; - attrs = { - provider_alias = oStr "Alias of the IdP this permission applies to."; - policy_type = oStr "Policy type (default 'client')."; - clients = oListStr "ClientIds of clients the permission is granted to."; - }; - }; - - realm_user_profiles = { - type = "keycloak_realm_user_profile"; - prefix = "realm_user_profile"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - # nested block inside a list element; wrapBlocks recurses through - # the list, so the dotted path matches. - blockAttrs = [ "attribute.permissions" ]; - description = "Per-realm user-profile schema (attribute declarations + groups). Keyed by an arbitrary label (one resource per realm)."; - attrs = { - unmanaged_attribute_policy = oStr "Policy for unmanaged attributes: 'DISABLED' (default), 'ENABLED', 'ADMIN_VIEW', or 'ADMIN_EDIT'."; - attribute = oListSub { - name = rStr "Attribute name."; - display_name = oStr "Display name (may be an i18n key)."; - multi_valued = oBool "Allow multiple values."; - group = oStr "Display group the attribute belongs to."; - enabled_when_scope = oListStr "Scopes that make the attribute available."; - required_for_roles = oListStr "Roles for which the attribute is required."; - required_for_scopes = oListStr "Scopes for which the attribute is required."; - # both are Required upstream; declared as oListStr (nullable, - # default null) so cleanNulls drops them when unset and apply - # errors -- vs `listOf str` which would silently default to [] - # and *strip* every role from keycloak's side. - permissions = oSub { - view = oListStr "Roles that can view the attribute (e.g. \"admin\", \"user\")."; - edit = oListStr "Roles that can edit the attribute."; - } "View / edit permissions for the attribute."; - validator = oListSub { - name = rStr "Validator id (e.g. \"length\", \"pattern\")."; - config = oAttrsStr "Validator-specific configuration."; - } "Validators applied to the attribute."; - annotations = oAttrsStr "Free-form display annotations."; - } "List of user-profile attribute declarations."; - group = oListSub { - name = rStr "Group name."; - display_header = oStr "Display header."; - display_description = oStr "Display description."; - annotations = oAttrsStr "Free-form display annotations."; - } "List of user-profile groups (used to cluster attributes in the UI)."; - }; - }; - - realm_client_policy_profiles = { - type = "keycloak_realm_client_policy_profile"; - prefix = "realm_client_policy_profile"; - nameAttr = "name"; - scope = null; - refs.realm = realmRef; - description = "Realm client-policy profile, listing executors that enforce a client policy."; - attrs = { - name = oStr "Profile name. Defaults to the attribute key."; - description = oStr "Profile description."; - executor = oListSub { - name = rStr "Executor provider-id (e.g. 'secure-client-uris')."; - configuration = oAttrsStr "Executor-specific configuration."; - } "List of executors run on policy evaluation."; - }; - }; - - realm_client_policy_profile_policies = { - type = "keycloak_realm_client_policy_profile_policy"; - prefix = "realm_client_policy_profile_policy"; - nameAttr = "name"; - scope = null; - refs = { - realm = realmRef; - profiles = { - attr = "profiles"; - targets = [ - { - collection = "realm_client_policy_profiles"; - field = "name"; - } - ]; - managedOnly = false; - required = true; - list = true; - description = "Names of client-policy profiles this policy applies. Each entry is a managed realm_client_policy_profile key (resolved to its name) or a literal profile name."; - }; - }; - description = "Realm client-policy policy binding a set of profiles to a set of conditions."; - attrs = { - name = oStr "Policy name. Defaults to the attribute key."; - description = oStr "Policy description."; - enabled = oBool "Is the policy enabled?"; - condition = oListSub { - name = rStr "Condition provider-id (e.g. 'client-roles')."; - configuration = oAttrsStr "Condition-specific configuration."; - } "List of conditions; the policy applies when all conditions match."; - }; - }; - - group_permissions = { - type = "keycloak_group_permissions"; - prefix = "group_permissions"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - group = { - attr = "group_id"; - targets = [ - { - collection = "groups"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed group these fine-grained permissions apply to."; - }; - }; - # every scope_* attr is a MaxItems:1 nested block. - blockAttrs = [ - "view_scope" - "manage_scope" - "view_members_scope" - "manage_members_scope" - "manage_membership_scope" - ]; - description = "Fine-grained authorization permissions for a group; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; - attrs = - let - scopePerm = oSub { - policies = oListStr "Names / ids of policies that apply to this scope."; - description = oStr "Description."; - decision_strategy = oStr "Decision strategy ('UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS')."; + # a role scoped to a client rather than to the realm. + client = anyClientOptionalRef; + }; + description = "Keycloak roles (realm-level by default), keyed by role name."; + }; + default_roles = { + type = "keycloak_default_roles"; + prefix = "default_roles"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + default_roles = { + attr = "default_roles"; + targets = [ + { + collection = "roles"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Role names auto-granted to every new user. Each entry is a managed role key (resolved to its name) or a literal role name (built-ins like 'offline_access' work as literals)."; }; - in - { - view_scope = scopePerm "View-scope permission block."; - manage_scope = scopePerm "Manage-scope permission block."; - view_members_scope = scopePerm "View-members-scope permission block."; - manage_members_scope = scopePerm "Manage-members-scope permission block."; - manage_membership_scope = scopePerm "Manage-membership-scope permission block."; }; - }; - - openid_client_permissions = { - type = "keycloak_openid_client_permissions"; - prefix = "openid_client_permissions"; - nameAttr = null; - scope = null; - refs = { - realm = realmRef; - client = { - attr = "client_id"; - targets = [ - { - collection = "openid_clients"; - field = "id"; - } - ]; - managedOnly = true; - required = true; - description = "Key of the managed openid_client these fine-grained permissions apply to."; - }; - }; - blockAttrs = [ - "view_scope" - "manage_scope" - "configure_scope" - "map_roles_scope" - "map_roles_client_scope_scope" - "map_roles_composite_scope" - "token_exchange_scope" - ]; - description = "Fine-grained authorization permissions on an openid_client; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; - attrs = - let - scopePerm = oSub { - policies = oListStr "Names / ids of policies that apply to this scope."; - description = oStr "Description."; - decision_strategy = oStr "Decision strategy ('UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS')."; + description = "Realm-level default roles auto-granted to new users, keyed by an arbitrary label."; + }; + groups = { + type = "keycloak_group"; + prefix = "group"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + parent = { + attr = "parent_id"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = true; + required = false; + description = "Optional parent group (key of another managed group) for nested groups."; + }; + organization = { + attr = "organization_id"; + targets = [ + { + collection = "organizations"; + field = "id"; + } + ]; + managedOnly = true; + required = false; + description = "Optional organization (key of a managed organization) this group belongs to."; }; - in - { - view_scope = scopePerm "View-scope permission block."; - manage_scope = scopePerm "Manage-scope permission block."; - configure_scope = scopePerm "Configure-scope permission block."; - map_roles_scope = scopePerm "Map-roles-scope permission block."; - map_roles_client_scope_scope = scopePerm "Map-roles-client-scope-scope permission block."; - map_roles_composite_scope = scopePerm "Map-roles-composite-scope permission block."; - token_exchange_scope = scopePerm "Token-exchange-scope permission block."; }; - }; - - users_permissions = { - type = "keycloak_users_permissions"; - prefix = "users_permissions"; - nameAttr = null; - scope = null; - refs.realm = realmRef; - blockAttrs = [ - "view_scope" - "manage_scope" - "map_roles_scope" - "manage_group_membership_scope" - "impersonate_scope" - "user_impersonated_scope" - ]; - description = "Fine-grained authorization permissions on the realm's users collection; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; - attrs = - let - scopePerm = oSub { - policies = oListStr "Names / ids of policies that apply to this scope."; - description = oStr "Description."; - decision_strategy = oStr "Decision strategy ('UNANIMOUS', 'AFFIRMATIVE', 'CONSENSUS')."; + description = "Keycloak groups, keyed by group name."; + }; + default_groups = { + type = "keycloak_default_groups"; + prefix = "default_groups"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + group_ids = { + attr = "group_ids"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Groups new users auto-join. Each entry is a managed group key (resolved to its id) or a literal group UUID."; + }; + }; + description = "Realm-level default groups auto-joined by new users, keyed by an arbitrary label."; + }; + group_memberships = { + type = "keycloak_group_memberships"; + prefix = "group_membership"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + group = { + attr = "group_id"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed group (services.keycloak.runtime.groups.) the members are added to."; + }; + members = { + attr = "members"; + targets = [ + { + collection = "users"; + field = "username"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Users to add to the group. Each entry is a managed user key (resolved to its username) or a literal username."; + }; + }; + description = "Keycloak group memberships, keyed by an arbitrary label."; + }; + group_roles = { + type = "keycloak_group_roles"; + prefix = "group_roles"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + group = { + attr = "group_id"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed group (services.keycloak.runtime.groups.) to assign roles to."; + }; + role_ids = { + attr = "role_ids"; + targets = [ + { + collection = "roles"; + field = "id"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Roles granted to the group. Each entry is a managed role key (resolved to its id) or a literal role UUID."; }; - in - { - view_scope = scopePerm "View-scope permission block."; - manage_scope = scopePerm "Manage-scope permission block."; - map_roles_scope = scopePerm "Map-roles-scope permission block."; - manage_group_membership_scope = scopePerm "Manage-group-membership-scope permission block."; - impersonate_scope = scopePerm "Impersonate-scope permission block."; - user_impersonated_scope = scopePerm "User-impersonated-scope permission block."; }; + description = "Role assignments for a group, keyed by an arbitrary label."; + }; + users = { + type = "keycloak_user"; + prefix = "user"; + nameAttr = "username"; + scope = null; + refs.realm = realmRef; + description = "Keycloak users, keyed by username (must be lowercase)."; + }; + user_roles = { + type = "keycloak_user_roles"; + prefix = "user_roles"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + user = { + attr = "user_id"; + targets = [ + { + collection = "users"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed user (services.keycloak.runtime.users.) to assign roles to."; + }; + role_ids = { + attr = "role_ids"; + targets = [ + { + collection = "roles"; + field = "id"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Roles granted to the user. Each entry is a managed role key (resolved to its id) or a literal role UUID."; + }; + }; + description = "Role assignments for a user, keyed by an arbitrary label."; + }; + user_groups = { + type = "keycloak_user_groups"; + prefix = "user_groups"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + user = { + attr = "user_id"; + targets = [ + { + collection = "users"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed user (services.keycloak.runtime.users.) to add to groups."; + }; + group_ids = { + attr = "group_ids"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Groups the user joins. Each entry is a managed group key (resolved to its id) or a literal group UUID."; + }; + }; + description = "Group memberships for a user, keyed by an arbitrary label."; + }; + openid_client_scopes = { + type = "keycloak_openid_client_scope"; + prefix = "openid_client_scope"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "OpenID client scopes (per-realm), keyed by scope name."; + }; + saml_client_scopes = { + type = "keycloak_saml_client_scope"; + prefix = "saml_client_scope"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "SAML client scopes (per-realm), keyed by scope name."; + }; + openid_clients = { + type = "keycloak_openid_client"; + prefix = "openid_client"; + nameAttr = "client_id"; + scope = null; + refs.realm = realmRef; + description = "OpenID Connect clients (per-realm), keyed by clientId."; + # Write-only twins of `client_secret`: they take an ephemeral value, + # which a rendered `.tf.json` cannot carry. `client_secretFile` covers + # the same ground through `LoadCredential=`. + omit = [ + "client_secret_wo" + "client_secret_wo_version" + ]; + }; + openid_client_default_scopes = { + type = "keycloak_openid_client_default_scopes"; + prefix = "openid_client_default_scopes"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = { + attr = "client_id"; + targets = [ + { + collection = "openid_clients"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed OpenID client (services.keycloak.runtime.openid_clients.) the scope binding applies to."; + }; + default_scopes = { + attr = "default_scopes"; + targets = [ + { + collection = "openid_client_scopes"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Scopes attached by default. Each entry is a managed openid_client_scope key (resolved to its name) or a literal scope name (built-ins like 'profile' / 'email' work as literals)."; + }; + }; + description = "Default OAuth2 scopes auto-attached to a client, keyed by an arbitrary label."; + }; + openid_client_optional_scopes = { + type = "keycloak_openid_client_optional_scopes"; + prefix = "openid_client_optional_scopes"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = { + attr = "client_id"; + targets = [ + { + collection = "openid_clients"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed OpenID client (services.keycloak.runtime.openid_clients.) the scope binding applies to."; + }; + optional_scopes = { + attr = "optional_scopes"; + targets = [ + { + collection = "openid_client_scopes"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Optionally-attached scopes. Each entry is a managed openid_client_scope key (resolved to its name) or a literal scope name."; + }; + }; + description = "Optional OAuth2 scopes available to a client, keyed by an arbitrary label."; + }; + openid_client_service_account_roles = { + type = "keycloak_openid_client_service_account_role"; + prefix = "openid_client_sa_role"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = { + attr = "client_id"; + targets = [ + { + collection = "openid_clients"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed target client whose role is granted."; + }; + }; + description = "Grant a per-client role to a service-account user, keyed by an arbitrary label."; + }; + openid_client_service_account_realm_roles = { + type = "keycloak_openid_client_service_account_realm_role"; + prefix = "openid_client_sa_realm_role"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + description = "Grant a realm-level role to a service-account user, keyed by an arbitrary label."; + }; + saml_clients = { + type = "keycloak_saml_client"; + prefix = "saml_client"; + nameAttr = "client_id"; + scope = null; + refs.realm = realmRef; + # signing_private_key isn't marked Sensitive upstream but is a + # private key; expose File so it stays out of the store. + description = "SAML clients (per-realm), keyed by clientId."; + extraSecrets = [ + "signing_private_key" + ]; + }; + saml_client_default_scopes = { + type = "keycloak_saml_client_default_scopes"; + prefix = "saml_client_default_scopes"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = { + attr = "client_id"; + targets = [ + { + collection = "saml_clients"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed SAML client (services.keycloak.runtime.saml_clients.) the scope binding applies to."; + }; + default_scopes = { + attr = "default_scopes"; + targets = [ + { + collection = "saml_client_scopes"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "SAML scopes attached by default. Each entry is a managed saml_client_scope key (resolved to its name) or a literal scope name."; + }; + }; + description = "Default SAML scopes auto-attached to a SAML client, keyed by an arbitrary label."; + }; + # OpenID protocol mappers: one collection per mapper type. all share + # realm + (client | client_scope) refs. + openid_user_attribute_protocol_mappers = { + type = "keycloak_openid_user_attribute_protocol_mapper"; + prefix = "openid_user_attribute_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps a user attribute to a claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_user_property_protocol_mappers = { + type = "keycloak_openid_user_property_protocol_mapper"; + prefix = "openid_user_property_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps a built-in user property (e.g. `email`, `username`) to a claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_group_membership_protocol_mappers = { + type = "keycloak_openid_group_membership_protocol_mapper"; + prefix = "openid_group_membership_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps group memberships to a claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_full_name_protocol_mappers = { + type = "keycloak_openid_full_name_protocol_mapper"; + prefix = "openid_full_name_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that emits the user's full name as a single claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_sub_protocol_mappers = { + type = "keycloak_openid_sub_protocol_mapper"; + prefix = "openid_sub_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper for the `sub` claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_hardcoded_claim_protocol_mappers = { + type = "keycloak_openid_hardcoded_claim_protocol_mapper"; + prefix = "openid_hardcoded_claim_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that adds a hardcoded claim with a fixed value."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_audience_protocol_mappers = { + type = "keycloak_openid_audience_protocol_mapper"; + prefix = "openid_audience_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that adds an audience to issued tokens (exactly one of `included_client_audience` / `included_custom_audience`)."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_audience_resolve_protocol_mappers = { + type = "keycloak_openid_audience_resolve_protocol_mapper"; + prefix = "openid_audience_resolve_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID audience-resolve mapper (derives audience from client roles)."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_hardcoded_role_protocol_mappers = { + type = "keycloak_openid_hardcoded_role_protocol_mapper"; + prefix = "openid_hardcoded_role_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that adds a hardcoded role to issued tokens."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_user_realm_role_protocol_mappers = { + type = "keycloak_openid_user_realm_role_protocol_mapper"; + prefix = "openid_user_realm_role_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps the user's realm roles to a claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_user_client_role_protocol_mappers = { + type = "keycloak_openid_user_client_role_protocol_mapper"; + prefix = "openid_user_client_role_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps the user's roles on a specific client to a claim."; + oneOfRefs = clientOrScopeOneOf; + }; + openid_user_session_note_protocol_mappers = { + type = "keycloak_openid_user_session_note_protocol_mapper"; + prefix = "openid_user_session_note_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = openidClientOptionalRef; + client_scope = openidClientScopeOptionalRef; + }; + description = "OpenID protocol mapper that maps a user session note to a claim."; + oneOfRefs = clientOrScopeOneOf; + requiredAttrs = [ + "session_note" + ]; + }; + saml_user_attribute_protocol_mappers = { + type = "keycloak_saml_user_attribute_protocol_mapper"; + prefix = "saml_user_attribute_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = samlClientOptionalRef; + client_scope = samlClientScopeOptionalRef; + }; + description = "SAML mapper that exposes a user attribute as a SAML attribute."; + oneOfRefs = clientOrScopeOneOf; + }; + saml_user_property_protocol_mappers = { + type = "keycloak_saml_user_property_protocol_mapper"; + prefix = "saml_user_property_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = samlClientOptionalRef; + client_scope = samlClientScopeOptionalRef; + }; + description = "SAML mapper that exposes a built-in user property as a SAML attribute."; + oneOfRefs = clientOrScopeOneOf; + }; + generic_protocol_mappers = { + type = "keycloak_generic_protocol_mapper"; + prefix = "generic_protocol_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = anyClientOptionalRef; + client_scope = anyClientScopeOptionalRef; + }; + description = "Generic protocol mapper escape hatch (for mappers without a dedicated typed resource)."; + oneOfRefs = clientOrScopeOneOf; + }; + generic_client_protocol_mappers = { + type = "keycloak_generic_client_protocol_mapper"; + prefix = "generic_client_protocol_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + client = anyClientOptionalRef; + client_scope = anyClientScopeOptionalRef; + }; + description = "Generic protocol mapper attached to a specific client (without a dedicated typed resource)."; + oneOfRefs = clientOrScopeOneOf; + }; + generic_role_mappers = { + type = "keycloak_generic_role_mapper"; + prefix = "generic_role_mapper"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = anyClientOptionalRef; + client_scope = anyClientScopeOptionalRef; + }; + description = "Generic role-scope mapper that attaches a role to a client / client scope, keyed by an arbitrary label."; + oneOfRefs = clientOrScopeOneOf; + }; + generic_client_role_mappers = { + type = "keycloak_generic_client_role_mapper"; + prefix = "generic_client_role_mapper"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = anyClientOptionalRef; + client_scope = anyClientScopeOptionalRef; + }; + description = "Generic role-scope mapper attached to a specific client (deprecated alias kept for completeness)."; + oneOfRefs = clientOrScopeOneOf; + }; + oidc_identity_providers = { + type = "keycloak_oidc_identity_provider"; + prefix = "oidc_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "Generic OIDC identity providers (per-realm), keyed by alias."; + # Write-only twins of `client_secret`: they take an ephemeral value, + # which a rendered `.tf.json` cannot carry. `client_secretFile` covers + # the same ground through `LoadCredential=`. + omit = [ + "client_secret_wo" + "client_secret_wo_version" + ]; + }; + saml_identity_providers = { + type = "keycloak_saml_identity_provider"; + prefix = "saml_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "SAML identity providers (per-realm), keyed by alias."; + }; + oidc_google_identity_providers = { + type = "keycloak_oidc_google_identity_provider"; + prefix = "oidc_google_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "Google OIDC identity providers (per-realm), keyed by alias (defaults to 'google')."; + }; + oidc_facebook_identity_providers = { + type = "keycloak_oidc_facebook_identity_provider"; + prefix = "oidc_facebook_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "Facebook OIDC identity providers (per-realm), keyed by alias (defaults to 'facebook')."; + }; + oidc_github_identity_providers = { + type = "keycloak_oidc_github_identity_provider"; + prefix = "oidc_github_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "GitHub OIDC identity providers (per-realm), keyed by alias (defaults to 'github')."; + }; + kubernetes_identity_providers = { + type = "keycloak_kubernetes_identity_provider"; + prefix = "kubernetes_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "Kubernetes OIDC identity providers (per-realm), keyed by alias."; + }; + hardcoded_attribute_identity_provider_mappers = { + type = "keycloak_hardcoded_attribute_identity_provider_mapper"; + prefix = "hardcoded_attribute_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Sets a hardcoded user (or session-note) attribute on every federated user."; + }; + hardcoded_group_identity_provider_mappers = { + type = "keycloak_hardcoded_group_identity_provider_mapper"; + prefix = "hardcoded_group_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Adds every federated user to a hardcoded group."; + }; + hardcoded_role_identity_provider_mappers = { + type = "keycloak_hardcoded_role_identity_provider_mapper"; + prefix = "hardcoded_role_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Grants a hardcoded role to every federated user."; + }; + attribute_importer_identity_provider_mappers = { + type = "keycloak_attribute_importer_identity_provider_mapper"; + prefix = "attribute_importer_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Imports an attribute / claim from the IdP onto the federated user."; + }; + attribute_to_role_identity_provider_mappers = { + type = "keycloak_attribute_to_role_identity_provider_mapper"; + prefix = "attribute_to_role_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Grants a role to federated users whose IdP attribute / claim matches a value."; + }; + user_template_importer_identity_provider_mappers = { + type = "keycloak_user_template_importer_identity_provider_mapper"; + prefix = "user_template_importer_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Derives the federated user's username from a Mustache-style template over IdP claims."; + }; + custom_identity_provider_mappers = { + type = "keycloak_custom_identity_provider_mapper"; + prefix = "custom_idp_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmAliasRef; + identity_provider = idpAliasRequiredRef; + }; + description = "Escape hatch for an IdP mapper implementation without a dedicated typed resource."; + }; + authentication_flows = { + type = "keycloak_authentication_flow"; + prefix = "authentication_flow"; + nameAttr = "alias"; + scope = null; + refs.realm = realmRef; + description = "Top-level authentication flows (per-realm), keyed by alias."; + }; + authentication_subflows = { + type = "keycloak_authentication_subflow"; + prefix = "authentication_subflow"; + nameAttr = "alias"; + scope = null; + refs = { + realm = realmRef; + parent_flow = { + attr = "parent_flow_alias"; + targets = [ + { + collection = "authentication_flows"; + field = "alias"; + } + { + collection = "authentication_subflows"; + field = "alias"; + } + ]; + managedOnly = false; + required = true; + description = "Alias of the parent flow (managed key or literal alias)."; + }; + }; + description = "Authentication subflows nested under a parent flow, keyed by alias."; + }; + authentication_executions = { + type = "keycloak_authentication_execution"; + prefix = "authentication_execution"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + parent_flow = { + attr = "parent_flow_alias"; + targets = [ + { + collection = "authentication_flows"; + field = "alias"; + } + { + collection = "authentication_subflows"; + field = "alias"; + } + ]; + managedOnly = false; + required = true; + description = "Alias of the parent flow / subflow (managed key or literal alias)."; + }; + }; + description = "Authentication executions inside a flow / subflow, keyed by an arbitrary label."; + }; + authentication_execution_configs = { + type = "keycloak_authentication_execution_config"; + prefix = "authentication_execution_config"; + nameAttr = "alias"; + scope = null; + refs = { + realm = realmRef; + execution = { + attr = "execution_id"; + targets = [ + { + collection = "authentication_executions"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed authentication_execution (services.keycloak.runtime.authentication_executions.) this config attaches to."; + }; + }; + description = "Per-execution configuration map, keyed by config alias."; + }; + authentication_bindings = { + type = "keycloak_authentication_bindings"; + prefix = "authentication_bindings"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + description = "Realm-level authentication flow bindings (browser / registration / direct grant / etc.), keyed by an arbitrary label."; + }; + openid_client_authorization_resources = { + type = "keycloak_openid_client_authorization_resource"; + prefix = "openid_client_authz_resource"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client (with authorization enabled) hosting this resource."; + }; + }; + description = "Authorization resources hosted on an openid_client's resource server."; + }; + openid_client_authorization_scopes = { + type = "keycloak_openid_client_authorization_scope"; + prefix = "openid_client_authz_scope"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client (with authorization enabled) hosting this scope."; + }; + }; + description = "Authorization scopes on an openid_client's resource server."; + }; + openid_client_authorization_permissions = { + type = "keycloak_openid_client_authorization_permission"; + prefix = "openid_client_authz_permission"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client (with authorization enabled) hosting this permission."; + }; + }; + description = "Authorization permissions tying resources/scopes to policies."; + }; + openid_client_aggregate_policies = { + type = "keycloak_openid_client_aggregate_policy"; + prefix = "openid_client_aggregate_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this aggregate policy."; + }; + }; + description = "Aggregate policy combining other policies under a decision strategy."; + }; + openid_client_client_policies = { + type = "keycloak_openid_client_client_policy"; + prefix = "openid_client_client_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access to a specific set of clients."; + requiredAttrs = [ + "decision_strategy" + ]; + }; + openid_client_authorization_client_scope_policies = { + type = "keycloak_openid_client_authorization_client_scope_policy"; + prefix = "openid_client_authz_client_scope_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access by client scope membership; each scope block is `{ id; required = false; }`."; + requiredAttrs = [ + "decision_strategy" + ]; + }; + openid_client_group_policies = { + type = "keycloak_openid_client_group_policy"; + prefix = "openid_client_group_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access by group membership; each group block is `{ id; path; extend_children; }`."; + }; + openid_client_role_policies = { + type = "keycloak_openid_client_role_policy"; + prefix = "openid_client_role_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access by realm or client role membership; each role block is `{ id; required = false; }`."; + requiredAttrs = [ + "decision_strategy" + ]; + }; + openid_client_time_policies = { + type = "keycloak_openid_client_time_policy"; + prefix = "openid_client_time_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access within a time window."; + }; + ldap_user_federations = { + type = "keycloak_ldap_user_federation"; + prefix = "ldap_user_federation"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "LDAP user federations (per-realm), keyed by name."; + }; + ldap_user_attribute_mappers = { + type = "keycloak_ldap_user_attribute_mapper"; + prefix = "ldap_user_attribute_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Maps a keycloak user attribute to an LDAP attribute."; + }; + ldap_group_mappers = { + type = "keycloak_ldap_group_mapper"; + prefix = "ldap_group_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Maps LDAP groups onto keycloak groups."; + }; + ldap_role_mappers = { + type = "keycloak_ldap_role_mapper"; + prefix = "ldap_role_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Maps LDAP roles onto keycloak realm or client roles."; + }; + ldap_hardcoded_role_mappers = { + type = "keycloak_ldap_hardcoded_role_mapper"; + prefix = "ldap_hardcoded_role_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Grants a hardcoded role to every LDAP-federated user."; + }; + ldap_hardcoded_attribute_mappers = { + type = "keycloak_ldap_hardcoded_attribute_mapper"; + prefix = "ldap_hardcoded_attribute_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Sets a hardcoded user attribute on every LDAP-federated user."; + }; + ldap_hardcoded_group_mappers = { + type = "keycloak_ldap_hardcoded_group_mapper"; + prefix = "ldap_hardcoded_group_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Adds every LDAP-federated user to a hardcoded group."; + }; + ldap_msad_user_account_control_mappers = { + type = "keycloak_ldap_msad_user_account_control_mapper"; + prefix = "ldap_msad_uac_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "MSAD userAccountControl integration mapper (enables / disables and locks out users)."; + }; + ldap_msad_lds_user_account_control_mappers = { + type = "keycloak_ldap_msad_lds_user_account_control_mapper"; + prefix = "ldap_msad_lds_uac_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "MSAD LDS userAccountControl integration mapper."; + }; + ldap_full_name_mappers = { + type = "keycloak_ldap_full_name_mapper"; + prefix = "ldap_full_name_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Splits/joins a single LDAP full-name attribute into keycloak's first / last name fields."; + }; + ldap_custom_mappers = { + type = "keycloak_ldap_custom_mapper"; + prefix = "ldap_custom_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Escape hatch for an LDAP mapper implementation without a dedicated typed resource."; + }; + custom_user_federations = { + type = "keycloak_custom_user_federation"; + prefix = "custom_user_federation"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "Custom user federation backed by a JPA / SPI provider."; + }; + realm_keystore_aes_generateds = { + type = "keycloak_realm_keystore_aes_generated"; + prefix = "realm_keystore_aes_generated"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "AES keystore generated by Keycloak."; + }; + realm_keystore_ecdsa_generateds = { + type = "keycloak_realm_keystore_ecdsa_generated"; + prefix = "realm_keystore_ecdsa_generated"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "ECDSA keystore generated by Keycloak."; + }; + realm_keystore_hmac_generateds = { + type = "keycloak_realm_keystore_hmac_generated"; + prefix = "realm_keystore_hmac_generated"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "HMAC keystore generated by Keycloak."; + }; + realm_keystore_java_keystores = { + type = "keycloak_realm_keystore_java_keystore"; + prefix = "realm_keystore_java_keystore"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "Keystore backed by a Java KeyStore (JKS) file."; + extraSecrets = [ + "key_password" + "keystore_password" + ]; + }; + realm_keystore_rsas = { + type = "keycloak_realm_keystore_rsa"; + prefix = "realm_keystore_rsa"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + # private_key and certificate are PEM material; expose File + # for both even though only private_key is technically secret. + description = "Keystore backed by an externally-provided RSA private key / certificate pair."; + extraSecrets = [ + "certificate" + "private_key" + ]; + }; + realm_keystore_rsa_generateds = { + type = "keycloak_realm_keystore_rsa_generated"; + prefix = "realm_keystore_rsa_generated"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "RSA keystore generated by Keycloak."; + }; + hardcoded_attribute_mappers = { + type = "keycloak_hardcoded_attribute_mapper"; + prefix = "hardcoded_attribute_mapper"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + ldap_user_federation = ldapFederationIdRef; + }; + description = "Sets a hardcoded user attribute on every federated user. Distinct from ldap_hardcoded_attribute_mapper and hardcoded_attribute_identity_provider_mapper."; + }; + openid_client_user_policies = { + type = "keycloak_openid_client_user_policy"; + prefix = "openid_client_user_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access to a specific set of users."; + }; + required_actions = { + type = "keycloak_required_action"; + prefix = "required_action"; + nameAttr = "alias"; + scope = null; + refs.realm = realmRef; + description = "Realm required actions (per-realm), keyed by alias."; + }; + realm_events = { + type = "keycloak_realm_events"; + prefix = "realm_events"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + description = "Per-realm event logging configuration, keyed by an arbitrary label."; + }; + realm_localizations = { + type = "keycloak_realm_localization"; + prefix = "realm_localization"; + nameAttr = "locale"; + scope = null; + refs.realm = realmRef; + description = "Per-realm i18n message bundle, keyed by locale."; + }; + realm_default_client_scopes = { + type = "keycloak_realm_default_client_scopes"; + prefix = "realm_default_client_scopes"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + default_scopes = { + attr = "default_scopes"; + targets = [ + { + collection = "openid_client_scopes"; + field = "name"; + } + { + collection = "saml_client_scopes"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Scope names auto-attached as default to every new client. Each entry is a managed openid/saml client_scope key (resolved to its name) or a literal scope name."; + }; + }; + description = "Realm-wide default client-scope binding (set of scope names), keyed by an arbitrary label. Distinct from realms..default_default_client_scopes, which is a free-form realm attribute."; + }; + realm_optional_client_scopes = { + type = "keycloak_realm_optional_client_scopes"; + prefix = "realm_optional_client_scopes"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + optional_scopes = { + attr = "optional_scopes"; + targets = [ + { + collection = "openid_client_scopes"; + field = "name"; + } + { + collection = "saml_client_scopes"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Scope names available as optional to every new client. Each entry is a managed openid/saml client_scope key (resolved to its name) or a literal scope name."; + }; + }; + description = "Realm-wide optional client-scope binding (set of scope names), keyed by an arbitrary label."; + }; + organizations = { + type = "keycloak_organization"; + prefix = "organization"; + nameAttr = "name"; + scope = null; + refs.realm = realmAliasRef; + description = "Keycloak organizations (per-realm, requires the organizations feature), keyed by name."; + }; + identity_provider_token_exchange_scope_permissions = { + type = "keycloak_identity_provider_token_exchange_scope_permission"; + prefix = "idp_token_exchange_perm"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + description = "Per-IdP token-exchange permission policy granting a set of clients access to the IdP's token-exchange scope."; + }; + realm_user_profiles = { + type = "keycloak_realm_user_profile"; + prefix = "realm_user_profile"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + # nested block inside a list element; wrapBlocks recurses through + # the list, so the dotted path matches. + description = "Per-realm user-profile schema (attribute declarations + groups). Keyed by an arbitrary label (one resource per realm)."; + }; + realm_client_policy_profiles = { + type = "keycloak_realm_client_policy_profile"; + prefix = "realm_client_policy_profile"; + nameAttr = "name"; + scope = null; + refs.realm = realmRef; + description = "Realm client-policy profile, listing executors that enforce a client policy."; + }; + realm_client_policy_profile_policies = { + type = "keycloak_realm_client_policy_profile_policy"; + prefix = "realm_client_policy_profile_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + profiles = { + attr = "profiles"; + targets = [ + { + collection = "realm_client_policy_profiles"; + field = "name"; + } + ]; + managedOnly = false; + required = true; + list = true; + description = "Names of client-policy profiles this policy applies. Each entry is a managed realm_client_policy_profile key (resolved to its name) or a literal profile name."; + }; + }; + description = "Realm client-policy policy binding a set of profiles to a set of conditions."; + }; + group_permissions = { + type = "keycloak_group_permissions"; + prefix = "group_permissions"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + group = { + attr = "group_id"; + targets = [ + { + collection = "groups"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed group these fine-grained permissions apply to."; + }; + }; + # every scope_* attr is a MaxItems:1 nested block. + description = "Fine-grained authorization permissions for a group; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; + }; + openid_client_permissions = { + type = "keycloak_openid_client_permissions"; + prefix = "openid_client_permissions"; + nameAttr = null; + scope = null; + refs = { + realm = realmRef; + client = { + attr = "client_id"; + targets = [ + { + collection = "openid_clients"; + field = "id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client these fine-grained permissions apply to."; + }; + }; + description = "Fine-grained authorization permissions on an openid_client; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; + }; + users_permissions = { + type = "keycloak_users_permissions"; + prefix = "users_permissions"; + nameAttr = null; + scope = null; + refs.realm = realmRef; + description = "Fine-grained authorization permissions on the realm's users collection; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; + }; }; }; + inherit (generated) resourceTypes; keycloakTfConfig = genlib.mkTfConfig { inherit @@ -3062,7 +1754,7 @@ let tokenVar ; providerName = "keycloak"; - runtimePrefix = "services.keycloak.runtime"; + inherit runtimePrefix; extraSensitiveVars = [ clientIdVar ]; providerBlock = cfg: { url = cfg.baseUrl; @@ -3080,6 +1772,7 @@ in keycloakTfConfig clientIdVar ; + inherit (generated) checks coverage; resourceOptions = genlib.resourceOptions resourceTypes; mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; }); } diff --git a/services/keycloak/module.nix b/services/keycloak/module.nix index 427ed25..8b870d9 100644 --- a/services/keycloak/module.nix +++ b/services/keycloak/module.nix @@ -1,7 +1,11 @@ +# `nixTfSchema` is the schema-conversion library the resource surface is derived +# from; the flake injects it via `_module.args`, since a NixOS module cannot +# reach a flake input by path. { config, lib, pkgs, + nixTfSchema, ... }: let @@ -15,7 +19,7 @@ let cfg = config.services.keycloak.runtime; keycloak = config.services.keycloak; - tflib = import ./lib.nix { inherit pkgs; }; + tflib = import ./lib.nix { inherit pkgs nixTfSchema; }; defaultBaseUrl = "http://localhost:${toString keycloak.settings.http-port}"; From af9f9af588cf1cf2f33084b1380ee47985ccd9fb Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 18:05:40 +0200 Subject: [PATCH 15/18] feat(services/keycloak): support resources new in provider 5.8.0 Model the four resources provider 5.8.0 added, which the previous commit had to list as `unsupported` for eval to be green: - `oidc_openshift_v4_identity_providers` and `spiffe_identity_providers`, both per-realm IdPs keyed by alias, joining the six existing IdP collections as targets of the mapper `identity_provider` reference. - `openid_client_regex_policies`, an authorization policy matching a token claim against a regular expression; same realm and `resource_server` references as the other client policies. - `workflows`, a realm event trigger plus an ordered `step` list. `unsupported` is now empty and the coverage report reads 101 of 101. The option surface grows by 68 options; rendered fixtures are unchanged, since no fixture uses these resources. They are derived from the schema and checked at eval, but not exercised against a live Keycloak -- the existing VM tests cover the resources they always covered. Assisted-by: Claude:claude-opus-5 --- services/keycloak/lib.nix | 65 +++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/services/keycloak/lib.nix b/services/keycloak/lib.nix index daa95c6..fca16ab 100644 --- a/services/keycloak/lib.nix +++ b/services/keycloak/lib.nix @@ -131,7 +131,7 @@ let }; # IdP mappers reference an IdP by alias; the alias can belong to - # any of the six IdP collections. + # any of the eight IdP collections. idpAliasRequiredRef = { attr = "identity_provider_alias"; targets = [ @@ -159,6 +159,14 @@ let collection = "kubernetes_identity_providers"; field = "alias"; } + { + collection = "oidc_openshift_v4_identity_providers"; + field = "alias"; + } + { + collection = "spiffe_identity_providers"; + field = "alias"; + } ]; managedOnly = false; required = true; @@ -217,15 +225,6 @@ let # every sdk/v2 resource carries a synthetic `id`; it is the resource's own # identity, computed on apply, and nothing a configuration declares. omitEverywhere = [ "id" ]; - # resources the provider offers that this pairing does not model yet. The - # generator refuses to ignore a resource silently, so this list is the - # complete, reviewable statement of what is missing. - unsupported = { - keycloak_oidc_openshift_v4_identity_provider = "New in provider 5.8.0; not modelled yet."; - keycloak_openid_client_regex_policy = "New in provider 5.8.0; not modelled yet."; - keycloak_spiffe_identity_provider = "New in provider 5.8.0; not modelled yet."; - keycloak_workflow = "New in provider 5.8.0; not modelled yet."; - }; resources = { realms = { type = "keycloak_realm"; @@ -963,6 +962,22 @@ let refs.realm = realmAliasRef; description = "Kubernetes OIDC identity providers (per-realm), keyed by alias."; }; + oidc_openshift_v4_identity_providers = { + type = "keycloak_oidc_openshift_v4_identity_provider"; + prefix = "oidc_openshift_v4_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "OpenShift 4 OIDC identity providers (per-realm), keyed by alias (defaults to 'openshift-v4')."; + }; + spiffe_identity_providers = { + type = "keycloak_spiffe_identity_provider"; + prefix = "spiffe_idp"; + nameAttr = "alias"; + scope = null; + refs.realm = realmAliasRef; + description = "SPIFFE identity providers (per-realm), keyed by alias."; + }; hardcoded_attribute_identity_provider_mappers = { type = "keycloak_hardcoded_attribute_identity_provider_mapper"; prefix = "hardcoded_attribute_idp_mapper"; @@ -1290,6 +1305,28 @@ let }; description = "Policy granting access by group membership; each group block is `{ id; path; extend_children; }`."; }; + openid_client_regex_policies = { + type = "keycloak_openid_client_regex_policy"; + prefix = "openid_client_regex_policy"; + nameAttr = "name"; + scope = null; + refs = { + realm = realmRef; + resource_server = { + attr = "resource_server_id"; + targets = [ + { + collection = "openid_clients"; + field = "resource_server_id"; + } + ]; + managedOnly = true; + required = true; + description = "Key of the managed openid_client hosting this policy."; + }; + }; + description = "Policy granting access when a token claim matches a regular expression."; + }; openid_client_role_policies = { type = "keycloak_openid_client_role_policy"; prefix = "openid_client_role_policy"; @@ -1742,6 +1779,14 @@ let refs.realm = realmRef; description = "Fine-grained authorization permissions on the realm's users collection; each scope_* attr binds a scope to a `{ decision_strategy; policies; description; }` block."; }; + workflows = { + type = "keycloak_workflow"; + prefix = "workflow"; + nameAttr = "name"; + scope = null; + refs.realm = realmAliasRef; + description = "Realm workflows: an event trigger (`on`) plus an ordered list of `step` actions, keyed by workflow name."; + }; }; }; inherit (generated) resourceTypes; From 758f466fe528f77e431b59b4a1db869693cef2c0 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 18:06:41 +0200 Subject: [PATCH 16/18] docs(services/keycloak): document schema-derived resources The README read as though the option surface were hand-maintained, and gave no answer to "how do I move to a newer provider?" -- which is now a mechanical procedure with a check that reports exactly what changed. Points the resource tables at `keycloak-options-doc` and `keycloak-schema-coverage` as the authoritative lists, adds the four collections new in provider 5.8.0 to those tables, and adds a "Provider updates" section covering the bump procedure, what drift the check reports, and `forceOptional` as the release valve for a newly-required attribute. Assisted-by: Claude:claude-opus-5 --- services/keycloak/README.md | 56 +++++++++++++++++++++++++++++++------ 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/services/keycloak/README.md b/services/keycloak/README.md index d4815bf..e042898 100644 --- a/services/keycloak/README.md +++ b/services/keycloak/README.md @@ -73,6 +73,10 @@ attributes are **typed options** named after the upstream resource's snake_case attributes (validated at `nix flake check`; an unknown name or wrong type is a build error). +Those options are **derived from the provider's own schema** rather than +hand-written, so they track the pinned provider exactly — see +[Provider updates](#provider-updates). + ### Self-bootstrap: one realm ```nix @@ -230,6 +234,14 @@ a reference accepts either a managed key or a literal name/id (the `managedOnly = false` form), built-in / external values just pass through. +What each collection accepts comes from the vendored provider schema, not +from the tables below: the authoritative option list is + +```sh +nix build .#checks.x86_64-linux.keycloak-options-doc # every option, typed +nix build .#checks.x86_64-linux.keycloak-schema-coverage # what is covered +``` + ### Realms | Option | `keycloak_*` resource | Key defaults | @@ -309,14 +321,16 @@ optional `client_scope` → openid/saml client_scopes, multi-target): ### Identity providers + mappers -| Option | `keycloak_*` resource | Key defaults | Reference inputs | -| ---------------------------------- | --------------------------------- | ------------ | ----------------------- | -| `oidc_identity_providers` | `oidc_identity_provider` | `alias` | `realm` (by realm name) | -| `saml_identity_providers` | `saml_identity_provider` | `alias` | `realm` (by realm name) | -| `oidc_google_identity_providers` | `oidc_google_identity_provider` | `alias` | `realm` (by realm name) | -| `oidc_facebook_identity_providers` | `oidc_facebook_identity_provider` | `alias` | `realm` (by realm name) | -| `oidc_github_identity_providers` | `oidc_github_identity_provider` | `alias` | `realm` (by realm name) | -| `kubernetes_identity_providers` | `kubernetes_identity_provider` | `alias` | `realm` (by realm name) | +| Option | `keycloak_*` resource | Key defaults | Reference inputs | +| -------------------------------------- | ------------------------------------- | ------------ | ----------------------- | +| `oidc_identity_providers` | `oidc_identity_provider` | `alias` | `realm` (by realm name) | +| `saml_identity_providers` | `saml_identity_provider` | `alias` | `realm` (by realm name) | +| `oidc_google_identity_providers` | `oidc_google_identity_provider` | `alias` | `realm` (by realm name) | +| `oidc_facebook_identity_providers` | `oidc_facebook_identity_provider` | `alias` | `realm` (by realm name) | +| `oidc_github_identity_providers` | `oidc_github_identity_provider` | `alias` | `realm` (by realm name) | +| `kubernetes_identity_providers` | `kubernetes_identity_provider` | `alias` | `realm` (by realm name) | +| `oidc_openshift_v4_identity_providers` | `oidc_openshift_v4_identity_provider` | `alias` | `realm` (by realm name) | +| `spiffe_identity_providers` | `spiffe_identity_provider` | `alias` | `realm` (by realm name) | Identity-provider mappers (`realm` + `identity_provider` → any IdP collection, multi-target with literal fallback): @@ -352,6 +366,7 @@ collection, multi-target with literal fallback): | `openid_client_client_policies` | `openid_client_client_policy` | `name` | | `openid_client_authorization_client_scope_policies` | `openid_client_authorization_client_scope_policy` | `name` | | `openid_client_group_policies` | `openid_client_group_policy` | `name` | +| `openid_client_regex_policies` | `openid_client_regex_policy` | `name` | | `openid_client_role_policies` | `openid_client_role_policy` | `name` | | `openid_client_time_policies` | `openid_client_time_policy` | `name` | | `openid_client_user_policies` | `openid_client_user_policy` | `name` | @@ -414,6 +429,31 @@ Other federation: | `realm_client_policy_profile_policies` | `realm_client_policy_profile_policy` | `name` | `realm`, `profiles` → realm_client_policy_profiles | | `group_permissions` | `group_permissions` | — | `realm`, `group` → groups | | `users_permissions` | `users_permissions` | — | `realm` | +| `workflows` | `workflow` | `name` | `realm` (by realm name) | + +## Provider updates + +The option surface is generated from `provider-schema.json`, a normalized +dump of the pinned provider's schema, committed next to `module.nix`. The +provider itself comes from nixpkgs, so it moves when the flake's `nixpkgs` +input does; after such a bump: + +```sh +nix run .#update-provider-schemas +nix flake check +``` + +The check names every difference the bump introduces: resources added or +removed, attributes added, removed or retyped, and any correction in +`lib.nix` that no longer matches the schema. A new resource must either be +modelled or listed in the pairing's `unsupported` set with a reason — it +cannot be ignored. Every resource `keycloak/keycloak` 5.8.0 offers is +currently modelled, so that set is empty. + +An upstream attribute that becomes **required** turns into a required +option, which fails evaluation for configurations that never set it. That +is usually the right signal, but `forceOptional` in the collection's +overlay is the release valve when it is not. ## State directory note From e890701c8fd5201fcda37c19915c50bb5055d675 Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 18:09:10 +0200 Subject: [PATCH 17/18] docs(CLAUDE.md): rewrite the provider contract for schema-driven generation The contract still told a new pairing to enumerate every provider resource by hand, which is no longer how either implemented pairing works and would produce a surface with no drift detection. Rewrites "Modeling the resource surface" around `mkResourceTypes`, with a table splitting what the schema derives from what the overlay must state, the correction fields and the rule that every overlay key names a real schema path. Adds "Vendored provider schema" (what is committed, how it is normalized, why never IFD), "Two schema dialects" (sdk/v2 `block_types` needing `[ { ... } ]` wrapping vs plugin-framework `nested_type`, and which pairing is which), and "Drift is a hard error" (the assertion set, `unsupported` with mandatory reasons, and the three checks). Records the derivation as a settled decision, and documents the four force-optional cases with the reason each exists -- the collection one especially, since an empty `listOf` would silently strip server-side state. Also updates the repository layout, the `lib.nix`/`module.nix` signatures, `fixtures.nix` and the rendered-fixtures snapshot, the README outline, and the Development section with the schema-update and before/after diff commands. Assisted-by: Claude:claude-opus-5 --- CLAUDE.md | 211 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 159 insertions(+), 52 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d253729..7203b25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,19 +31,20 @@ already covered by `services.authelia.*` — so it was dropped as a pairing.) ## Settled decisions -| Topic | Decision | -| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Executor | **OpenTofu** (nixpkgs `opentofu`, MPL 2.0 / free). `terraform` is BSL 1.1 / unfree and is **not** used. | -| Config authoring | Generate **`.tf.json`** directly from Nix (`builtins.toJSON`). No HCL, no terranix dependency. | -| Secrets | **systemd `LoadCredential=`** is the blessed path. Generated config references the secret as a `sensitive` Terraform variable; never literal secrets. sops-nix/agenix, if used, only supply the source that feeds `LoadCredential=`. | -| Reconciliation | **Run-once**: a `Type=oneshot` unit ordered `After=` the primary unit + readiness probe, runs `init` + `apply -auto-approve`. Re-applies on config change via `restartTriggers`. **No** drift timer. A failed apply fails _that unit_ visibly (`systemctl status`) and does **not** tear down the service. | -| State | **Local, per-host only.** Terraform state lives under the **base service's primary state directory** (e.g. `services.forgejo.stateDir` → `/var/lib/forgejo`), co-located with the service it configures. No remote backends. | -| Module namespace | Under the base service as **`services..runtime.*`** (e.g. `services.forgejo.runtime.repositories`), so the pairing reads as a transparent extension of the upstream `services.` module. | -| Formatter | **treefmt** driving **nixfmt**. Formatter is the single source of layout truth — run it, never hand-format. | -| CI | **GitHub Actions**: `nix flake check` on push + PR, Nix provided by Determinate Systems `nix-installer-action`. Workflow is Forgejo-Actions-compatible (same syntax) if hosting moves there. | -| License | **MIT** (matches nixpkgs ecosystem norms; permissive). | -| Toolchain pin | Flake `nixpkgs` input tracks **`nixos-unstable`** (the verified provider/service versions live there); minimum **Nix ≥ 2.18** for the stable flake CLI + `nix flake check`. | -| Commits | **Conventional Commits**, **atomic** (one self-contained conceptual change per commit; the tree builds/passes at every commit), linear history (rebase/squash, no merge commits). VCS is the colocated `jj`/`git` checkout. | +| Topic | Decision | +| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Executor | **OpenTofu** (nixpkgs `opentofu`, MPL 2.0 / free). `terraform` is BSL 1.1 / unfree and is **not** used. | +| Config authoring | Generate **`.tf.json`** directly from Nix (`builtins.toJSON`). No HCL, no terranix dependency. | +| Secrets | **systemd `LoadCredential=`** is the blessed path. Generated config references the secret as a `sensitive` Terraform variable; never literal secrets. sops-nix/agenix, if used, only supply the source that feeds `LoadCredential=`. | +| Reconciliation | **Run-once**: a `Type=oneshot` unit ordered `After=` the primary unit + readiness probe, runs `init` + `apply -auto-approve`. Re-applies on config change via `restartTriggers`. **No** drift timer. A failed apply fails _that unit_ visibly (`systemctl status`) and does **not** tear down the service. | +| State | **Local, per-host only.** Terraform state lives under the **base service's primary state directory** (e.g. `services.forgejo.stateDir` → `/var/lib/forgejo`), co-located with the service it configures. No remote backends. | +| Module namespace | Under the base service as **`services..runtime.*`** (e.g. `services.forgejo.runtime.repositories`), so the pairing reads as a transparent extension of the upstream `services.` module. | +| Resource surface | **Derived from a vendored provider schema**, via the generic converters in [`nix-tf-schema`](https://git.fediversity.eu/fediversity/nix-tf-schema) (a source-only flake input). A pairing hand-writes only what the schema cannot carry: the reference graph, the NixOS-facing prose, and per-collection corrections. Provider drift is an eval-time error. | +| Formatter | **treefmt** driving **nixfmt**. Formatter is the single source of layout truth — run it, never hand-format. | +| CI | **GitHub Actions**: `nix flake check` on push + PR, Nix provided by Determinate Systems `nix-installer-action`. Workflow is Forgejo-Actions-compatible (same syntax) if hosting moves there. | +| License | **MIT** (matches nixpkgs ecosystem norms; permissive). | +| Toolchain pin | Flake `nixpkgs` input tracks **`nixos-unstable`** (the verified provider/service versions live there); minimum **Nix ≥ 2.18** for the stable flake CLI + `nix flake check`. | +| Commits | **Conventional Commits**, **atomic** (one self-contained conceptual change per commit; the tree builds/passes at every commit), linear history (rebase/squash, no merge commits). VCS is the colocated `jj`/`git` checkout. | ## Core mechanism @@ -77,72 +78,156 @@ Each service<->provider pairing lives in its own directory under `services/`. `services/forgejo` is the worked example: ``` -flake.nix # outputs: nixosModules (.default + per-pairing .), checks, formatter +flake.nix # outputs: nixosModules (.default + per-pairing .), packages, apps, checks, formatter treefmt.nix # treefmt + nixfmt config modules/ default.nix # aggregates per-pairing modules into nixosModules.default - lib/ # provider-agnostic helpers: tf-label/file, run-once OpenTofu reconciler + lib/ + default.nix # provider-agnostic helpers: tf-label/file, .tf.json generation, run-once OpenTofu reconciler + tf-schema.nix # schema-driven resourceTypes generator + drift assertions + render-fixtures.nix # renders a pairing's fixtures through the real option system (refactor snapshot) + schema-report.nix # the per-pairing coverage table, as a build artifact services/ # one directory per service<->provider pairing forgejo/ # worked example: the Forgejo <-> svalabs/forgejo pairing module.nix # NixOS module: services.forgejo.runtime options + systemd wiring (reconciler + token bootstrap) - lib.nix # provider specifics: wrapped OpenTofu executor + .tf.json generation (resource spec + ref resolution) + lib.nix # provider specifics: wrapped OpenTofu executor + the resource-surface overlay + provider-schema.json # normalized dump of the pinned provider's schema; the resource surface is derived from it + schema.nix # one line: fromJSON (readFile ./provider-schema.json) -- import is memoized, readFile is not pkg.nix # optional: vendor the provider when it's not in nixpkgs (here svalabs/forgejo via mkProvider) + fixtures.nix # the services.forgejo.runtime blocks the VM tests converge, shared with the rendered-fixtures package checks.nix # the pairing's checks attrset (nixosTest); merged into flake checks README.md # per-pairing usage docs + keycloak/ # the Keycloak <-> keycloak/keycloak pairing; same layout, no pkg.nix (provider is in nixpkgs) ``` ## Provider implementation contract `services/forgejo` is the template. A new pairing `services//` provides -`module.nix`, `lib.nix`, `checks.nix`, a `README.md`, and (only when the -provider is not in nixpkgs) `pkg.nix`. It **reuses** the provider-agnostic -helpers in `modules/lib` and **replicates** the forgejo `lib.nix` generation -pattern. Everything below is what the forgejo pairing encodes. +`module.nix`, `lib.nix`, `provider-schema.json`, `schema.nix`, `fixtures.nix`, +`checks.nix`, a `README.md`, and (only when the provider is not in nixpkgs) +`pkg.nix`. It **reuses** the provider-agnostic helpers in `modules/lib` — +including the resource-surface generator — and only writes down what the +provider schema cannot express. Everything below is what the forgejo and +keycloak pairings encode. ### Wiring a new pairing -- Export it from the flake: add `nixosModules. = ./services//module.nix;`, - add the directory to `modules/default.nix`'s `imports` (so it also joins the - aggregate `nixosModules.default`), and merge +- Export it from the flake: add + `nixosModules. = withSchemaLib ./services//module.nix;`, add an entry + to `pairingLibs` (which is what feeds the schema, coverage, options-doc and + rendered-fixture outputs), add the directory to `modules/default.nix`'s + `imports` (so it also joins the aggregate `nixosModules.default`), and merge `import ./services//checks.nix { inherit pkgs self; }` into the flake's `checks`. -- Everything (`checks`, `formatter`) is produced for both `x86_64-linux` and - `aarch64-linux` via `forAllSystems`. +- `withSchemaLib` sets `_module.args.nixTfSchema`: a NixOS module cannot reach a + flake input by path, so the schema library is threaded in as a module argument. +- Everything (`packages`, `checks`, `formatter`) is produced for both + `x86_64-linux` and `aarch64-linux`. + +### Vendored provider schema + +- `provider-schema.json` is a normalized `tofu providers schema -json` dump of + the pinned provider: `jq -S`, data sources and the top-level `provider` block + dropped, `source`/`version` injected (the dump itself carries no version). It + is committed, and `nix run .#update-provider-schemas` regenerates it. +- **Never IFD.** The flake evaluates for `aarch64-linux` as well, and IFD would + mean running a foreign-arch provider binary during eval. Provider schemas are + platform-independent, so one committed JSON per provider version is correct. +- `schema.nix` is a one-line `fromJSON (readFile ./provider-schema.json)`. Nix + memoizes `import ` but not `readFile`, and `lib.nix` is instantiated + ~10 times per `nix flake check`. ### `lib.nix` — provider specifics -- Signature `{ pkgs }:`; pulls `genlib = import ../../modules/lib { inherit pkgs; }` - and returns `{ resourceTypes; resourceOptions; TfConfig; mkReconcileService; … }`. +- Signature `{ pkgs, nixTfSchema }:`; pulls + `genlib = import ../../modules/lib { inherit pkgs; }` and + `tfSchema = import ../../modules/lib/tf-schema.nix { inherit pkgs nixTfSchema; }`, + and returns + `{ resourceTypes; resourceOptions; coverage; TfConfig; mkReconcileService; provider; providerSource; … }`. - Specializes the generic reconciler with the provider's executor and token variable: `mkReconcileService = args: genlib.mkReconcileService (args // { inherit executor tokenVar; });`. - `executor = pkgs.opentofu.withPlugins (_: [ provider ])`. `tokenVar` is the `sensitive` Terraform variable **and** `LoadCredential` id carrying the admin - token (e.g. `forgejo_api_token`). -- The `.tf.json` generation engine (`TfConfig`, reference resolution) lives - **here, per-provider**, modeled on forgejo — `modules/lib` carries only the - reusable `tfLabel`, `tfJsonFile`, and `mkReconcileService`. + credential (e.g. `forgejo_api_token`). +- `.tf.json` generation is `genlib.mkTfConfig` — shared, not per-provider. What + is per-provider is the resource-surface overlay below. ### Modeling the resource surface -- Enumerate every provider resource in a `resourceTypes` record: - `{ type = "_"; prefix; nameAttr; scope; refs; secrets; requiredSecrets; attrs; description; }` - — `prefix` is a unique label prefix, `nameAttr` the attribute defaulted from - the collection key (or `null`), `scope` the token scope(s) the resource needs, - `refs` its parent links, `secrets` its secret-valued attributes, - `requiredSecrets` those of them the provider requires, `attrs` the typed - option for every settable attribute. +- The surface is **derived from `provider-schema.json`**, not hand-written: + `tfSchema.mkResourceTypes { schema; provider; source; runtimePrefix; resources; unsupported ? {}; omitEverywhere ? []; }` + returns `{ resourceTypes; checks; coverage; }` in exactly the shape + `modules/lib/default.nix` already consumes. Split of responsibility: + + | Derived from the schema | Hand-written in the overlay | + | ------------------------------------------------------ | ---------------------------------------------------------- | + | every settable attribute, its Nix type and optionality | the collection name, `type`, `prefix`, `nameAttr`, `scope` | + | nested blocks as submodules, `blockAttrs` wrapping | `refs` — the reference graph, which no schema carries | + | `secrets` (schema `sensitive`) and `requiredSecrets` | `description` — the NixOS-facing prose | + | attribute descriptions (provider's own, else a stub) | corrections: see the next bullet | + +- Corrections a collection may declare, all of them optional and all validated + against the schema: `omit` (dotted paths to drop), `extraSecrets` / + `notSecrets` (attributes the schema mis-marks), `forceOptional`, + `requiredAttrs` (extra non-empty checks), `oneOfRefs` (Go-validator + `ExactlyOneOf`, absent from the schema), and `extraAttrs` (last-resort typed + overrides). Provider-wide dialect artifacts go in `omitEverywhere` — the + sdk/v2 synthetic `id`, say — not in per-collection `omit`. +- **Every overlay key must name a real schema path.** There is deliberately no + way to declare an option with no schema counterpart; `refs` is the only + exception, and that is what makes the drift check total. - Each resource is exposed as a **strictly typed** collection: `attrsOf (submodule { options = ++ ++ File; })` — **no - `freeformType`**. Every settable upstream attribute is a declared option typed - to what the provider accepts (string/bool/int/list/map, and `submodule` for - nested objects), so a wrong name, wrong type, or missing required field is an - eval-time error at `nix flake check`, not an apply-time one. Derive `attrs` - from the provider schema (`tofu providers schema -json`); omit computed / - output-only attributes. Required attributes are declared without a default; - optional ones are `nullOr T` defaulting to `null` (dropped from the generated - JSON when unset). The attrset key becomes the Terraform label and defaults - `nameAttr`. Reference inputs and `File` secret inputs are declared - separately (they are resolved/rerouted at generation, not passed through). + `freeformType`**. A wrong name, wrong type, or missing required field is an + eval-time error at `nix flake check`, not an apply-time one. Computed / + output-only attributes are dropped. The attrset key becomes the Terraform + label and defaults `nameAttr`. Reference inputs and `File` secret inputs + are declared separately (they are resolved/rerouted at generation, not passed + through). +- Required attributes are declared without a default; optional ones are + `nullOr T` defaulting to `null` (dropped from the generated JSON when unset). + Four cases are forced optional even when the schema says required: + - **`nameAttr`** — must be nullable so the attrset key can fill it; the + non-empty requirement is re-imposed after injection via `requiredAttrs`. + - **secrets** — the `File` sibling may satisfy them instead. + - **collection-typed** — `listOf`/`attrsOf` default to `[]`/`{}` and cannot + express "unset"; an empty value would silently strip server-side state. + - **explicit `forceOptional`** — the release valve when a provider bump makes + an attribute required and existing configurations must keep evaluating. + +### Two schema dialects + +Which one a provider speaks decides how nested objects are read, and both +readers are exercised in-tree: + +- **terraform-plugin-sdk/v2** (keycloak): nested objects live in + `block.block_types.` with a `nesting_mode`. A `list`/`set` block with + `max_items == 1` is a _singleton block_ that Terraform reads as a one-element + list, so it must render as `[ { … } ]` — that is what `blockAttrs` is for, and + it is derived, never listed by hand. Every sdk/v2 resource also carries a + synthetic `id`. +- **terraform-plugin-framework** (forgejo): nested objects live in + `block.attributes..nested_type` with `nesting_mode: "single"` and encode as + plain objects — no `blockAttrs` at all. + +### Drift is a hard error + +- Eval-time assertions live in the generator's `checks` and fire the moment + `resourceTypes` is forced. Identity: the schema's `source` and `version` match + the packaged provider. Overlay → schema: every `type`, `nameAttr`, ref `attr`, + correction entry and `extraAttrs` key names something the schema declares; + `prefix`es and `type`s are unique. Schema → overlay: every resource the + provider offers is either modelled or listed in `unsupported` with a non-empty + reason. A provider bump therefore names every new resource in the error. +- `-schema-coverage` forces those assertions on their own, so drift fails a + check that names it rather than whichever VM test happens to eval first, and + renders the coverage table as the review artifact. +- `-schema-current` is the authoritative one: it re-extracts the schema in + a sandbox and diffs it against the vendored file, so a provider that changes a + schema without changing its version still fails CI. +- `-options-doc` is the user-facing option surface as `options.json`; build + it before and after a change and diff, since rendered `.tf.json` alone cannot + show an option nobody sets. - Parent links are named by the **key of another managed resource** and resolved to `${type.label.field}` interpolations — this both wires the numeric `*_id` attributes a user cannot know _and_ orders `tofu apply`. A `refs` entry is @@ -157,7 +242,8 @@ pattern. Everything below is what the forgejo pairing encodes. ### Secrets — per-resource credential indirection -- Mark secret attributes in `resourceTypes..secrets`. Each gets an +- Secret attributes come from the schema's `sensitive` flag (with `extraSecrets` + / `notSecrets` to correct it), at any nesting depth. Each gets an `File` option taking a **host file path** (a string path resolved on the target, _never_ a Nix store path). When set, generation emits `${var.}` + a `sensitive` variable and collects an `id → host path` pair (id @@ -183,8 +269,8 @@ pattern. Everything below is what the forgejo pairing encodes. ### `module.nix` — the NixOS module -- `{ config, lib, pkgs, ... }:`; `cfg = config.services..runtime`; - `tflib = import ./lib.nix { inherit pkgs; }`. +- `{ config, lib, pkgs, nixTfSchema, ... }:`; `cfg = config.services..runtime`; + `tflib = import ./lib.nix { inherit pkgs nixTfSchema; }`. - Options live under `services..runtime` — so the pairing reads as a transparent extension of the upstream `services.` module: `enable` (`mkEnableOption`), `baseUrl` (default = the local instance), `tokenFile` @@ -214,6 +300,10 @@ pattern. Everything below is what the forgejo pairing encodes. value reaches the service; the literal is absent from the generated `.tf.json`). Use `specialisation` for config-change cases; size `virtualisation` for the service. No mocks. +- The `services..runtime` blocks the tests converge live in `fixtures.nix`, + not inline. `-rendered-fixtures` renders exactly those through the real + option system and renderer, so diffing that package before and after a change + to the resource surface proves the wire format is untouched. ### `pkg.nix` — vendoring (only when the provider is not in nixpkgs) @@ -224,7 +314,9 @@ pattern. Everything below is what the forgejo pairing encodes. ### Docs & comments - Ship a `README.md` per pairing: Installation → Configuration examples (several - distinct use cases) → Module options → Resources table → Security note. + distinct use cases) → Module options → Resources table → Provider updates → + Security note. The resources table is a map, not the contract: point at + `-options-doc` and `-schema-coverage` as the authoritative lists. - Every `.nix` file opens with a header comment stating its role and the _why_, not just the _what_. @@ -249,6 +341,21 @@ nix fmt # treefmt -> nixfmt across the tree nix develop # devshell (curl, jq) ``` +After a nixpkgs bump moves a provider (or a `pkg.nix` bump does): + +```sh +nix run .#update-provider-schemas # refresh every services//provider-schema.json +nix flake check # now names every resource and attribute that changed +``` + +Static evidence for a change to a resource surface, both diffed before/after: + +```sh +nix build .#-rendered-fixtures # the wire format -- an empty diff means no behaviour change +nix build .#checks..-options-doc # the user-facing option surface +nix build .#checks..-schema-coverage # what is modelled, and what is deliberately not +``` + ## Verification standard Behavior is proven with **NixOS VM integration tests** From 671b033680a17b12b1fe5afed1196cc3db416bff Mon Sep 17 00:00:00 2001 From: cinereal Date: Mon, 3 Aug 2026 18:09:58 +0200 Subject: [PATCH 18/18] docs(CLAUDE.md): correct the stale keycloak status rows The header and the pairings table still called Keycloak "designed, not yet built" at provider 5.7.0. It has been implemented for a while: all 101 resources of provider 5.8.0 are modelled and six VM tests prove them against a live instance. Also names it as the pairing to read for the sdk/v2 schema dialect and for a large resource surface, which forgejo (plugin-framework, 15 resources) cannot demonstrate. Assisted-by: Claude:claude-opus-5 --- CLAUDE.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7203b25..d22e18e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,10 +1,11 @@ # CLAUDE.md -> Status: the pattern is implemented. **`services/forgejo` is the worked -> reference pairing** — new pairings are modeled on it, and the "Provider -> implementation contract" below is exactly what it encodes. Grafana and -> Keycloak (see "Target pairings") are designed but not yet built; do not -> present them as implemented. +> Status: the pattern is implemented, by two pairings. **`services/forgejo` is +> the worked reference pairing** — new pairings are modeled on it, and the +> "Provider implementation contract" below is exactly what it encodes. +> `services/keycloak` is the second, and the one to read for the sdk/v2 schema +> dialect and for a large resource surface. Grafana (see "Target pairings") is +> designed but not yet built; do not present it as implemented. ## Purpose @@ -70,7 +71,7 @@ Provider reality verified against nixpkgs + the public registry: | ------------ | --------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Forgejo** | `forgejo` (svalabs/forgejo 1.5.0) | vendored in `services/forgejo/pkg.nix` (not in nixpkgs) | **Implemented — the reference pairing.** Dedicated Forgejo provider on the Forgejo Go SDK, tracking Forgejo's API as it diverges from Gitea (hard fork since 2024). Chosen over the in-nixpkgs `gitea` provider. Vendored via `terraform-providers.mkProvider`. | | **Grafana** | `grafana` (4.36.0) | `pkgs.terraform-providers.grafana` | Designed, not yet built. In-nixpkgs provider. | -| **Keycloak** | `keycloak` (5.7.0) | `pkgs.terraform-providers.keycloak` | Designed, not yet built. Full admin REST API (realms/clients/roles/scopes). Heavy JVM service — VM tests need extra memory and a generous readiness wait. | +| **Keycloak** | `keycloak` (5.8.0) | `pkgs.terraform-providers.keycloak` | **Implemented.** All 101 provider resources modelled, proven by six VM tests. Full admin REST API (realms/clients/roles/scopes). Heavy JVM service — VM tests need extra memory and a generous readiness wait. | ## Repository layout