fix: adding a -- separator for spi options (#40005)

* fix: adding a -- separator for spi options

closes: #39063

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* adding a warning for ambiguous spi options

also adding a note about the change

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
# Conflicts:
#	docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc

* updating docs to the new format

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
# Conflicts:
#	docs/guides/high-availability/examples/generated/keycloak-ispn.yaml
#	docs/guides/high-availability/examples/generated/keycloak.yaml

* internally using the new spi options

also adding a deprecation notice

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* Apply suggestions from code review

Co-authored-by: Martin Bartoš <mabartos@redhat.com>
Signed-off-by: Steven Hawkins <shawkins@redhat.com>

* correcting options output

adding + + inlining where needed

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

* adding test showing the env mapping with __

Signed-off-by: Steve Hawkins <shawkins@redhat.com>

---------

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
Signed-off-by: Steven Hawkins <shawkins@redhat.com>
Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Steven Hawkins 2025-06-13 10:13:53 -04:00 committed by GitHub
parent 2c65234c8f
commit 76bc9fadcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 325 additions and 168 deletions

View File

@ -235,6 +235,13 @@ public class Config {
Scope scope(String... scope);
/**
* @deprecated since 26.3.0, to be removed
*
* <br>Was introduced for testing purposes and was not fully / correctly implements
* across Scope implementations
*/
@Deprecated
Set<String> getPropertyNames();
}
}

View File

@ -51,7 +51,7 @@ Supported password hashing algorithms are shown in the following table.
It is highly recommended to use Argon2 when possible as it has significantly less CPU requirements compared to PBKDF2, while
at the same time being more secure.
The default password hashing algorithm for the server can be configured with `--spi-password-hashing-provider-default=<algorithm>`.
The default password hashing algorithm for the server can be configured with `--spi-password-hashing--provider-default=<algorithm>`.
To prevent excessive memory and CPU usage, the parallel computation of hashes by Argon2 is by default limited to the number of cores available to the JVM.
To configure the Argon2 hashing provider, use its provider options.
@ -141,14 +141,14 @@ Password must not be in a blacklist file.
* The value of the blacklist file must be the name of the blacklist file, for example, `100k_passwords.txt`.
* Blacklist files resolve against `+${kc.home.dir}/data/password-blacklists/+` by default. Customize this path using:
** The `keycloak.password.blacklists.path` system property.
** The `blacklistsPath` property of the `passwordBlacklist` policy SPI configuration. To configure the blacklist folder using the CLI, use `--spi-password-policy-password-blacklist-blacklists-path=/path/to/blacklistsFolder`.
** The `blacklistsPath` property of the `passwordBlacklist` policy SPI configuration. To configure the blacklist folder using the CLI, use `--spi-password-policy--password-blacklist--blacklists-path=/path/to/blacklistsFolder`.
.A note about False Positives
The current implementation uses a BloomFilter for fast and memory efficient containment checks, such as whether a given password is contained in a blacklist, with the possibility for false positives.
* By default a false positive probability of `0.01%` is used.
* To change the false positive probability by CLI configuration, use `--spi-password-policy-password-blacklist-false-positive-probability=0.00001`.
* To change the false positive probability by CLI configuration, use `--spi-password-policy--password-blacklist--false-positive-probability=0.00001`.
[[maximum-authentication-age]]
===== Maximum Authentication Age

View File

@ -35,9 +35,9 @@ You can now view admin events.
.Admin events
image:images/admin-events.png[Admin events]
When the `Include Representation` switch is ON, it can lead to storing a lot of information in the database. You can set a maximum length of the representation by using the `--spi-events-store-jpa-max-field-length` argument. This setting is useful if you want to adhere to the underlying storage limitation. For example:
When the `Include Representation` switch is ON, it can lead to storing a lot of information in the database. You can set a maximum length of the representation by using the `--spi-events-store--jpa--max-field-length` argument. This setting is useful if you want to adhere to the underlying storage limitation. For example:
[source,bash]
----
kc.[sh|bat] --spi-events-store-jpa-max-field-length=2500
kc.[sh|bat] --spi-events-store--jpa--max-field-length=2500
----

View File

@ -184,7 +184,7 @@ To change the log level used by the Logging Event listener, add the following:
[source,bash]
----
bin/kc.[sh|bat] start --spi-events-listener-jboss-logging-success-level=info --spi-events-listener-jboss-logging-error-level=error
bin/kc.[sh|bat] start --spi-events-listener-jboss-logging-success-level=info --spi-events-listener--jboss-logging--error-level=error
----
The valid values for log levels are `debug`, `info`, `warn`, `error`, and `fatal`.
@ -226,15 +226,15 @@ To enable the Email Listener:
.Event listeners
image:images/event-listeners.png[Event listeners]
You can exclude events by using the `--spi-events-listener-email-exclude-events` argument. For example:
You can exclude events by using the `--spi-events-listener--email--exclude-events` argument. For example:
[source,bash]
----
kc.[sh|bat] --spi-events-listener-email-exclude-events=UPDATE_CREDENTIAL,REMOVE_CREDENTIAL
kc.[sh|bat] --spi-events-listener--email--exclude-events=UPDATE_CREDENTIAL,REMOVE_CREDENTIAL
----
To enable optional events, use the following command:
[source,bash]
----
kc.[sh|bat] --spi-events-listener-email-include-events=USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR,USER_DISABLED_BY_PERMANENT_LOCKOUT
kc.[sh|bat] --spi-events-listener--email--include-events=USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR,USER_DISABLED_BY_PERMANENT_LOCKOUT
----

View File

@ -14,11 +14,11 @@ If you have a parent group and a child group, and a user that belongs only to th
The hierarchy of a group is sometimes represented using the group path. The path is the complete list of names that represents the hierarchy of a specific group, from top to bottom and separated by slashes `/` (similar to files in a File System). For example a path can be `/top/level1/level2` which means that `top` is a top level group and is parent of `level1`, which in turn is parent of `level2`. This path represents unambiguously the hierarchy for the group `level2`.
Because of historical reasons {project_name}, does not escape slashes in the group name itself. Therefore a group named `level1/group` under `top` uses the path `/top/level1/group`, which is misleading. {project_name} can be started with the option `--spi-group-jpa-escape-slashes-in-group-path` to `true` and then the slashes in the name are escaped with the character `~`. The escape char marks that the slash is part of the name and has no hierarchical meaning. The previous path example would be `/top/level1~/group` when escaped.
Because of historical reasons {project_name}, does not escape slashes in the group name itself. Therefore a group named `level1/group` under `top` uses the path `/top/level1/group`, which is misleading. {project_name} can be started with the option `--spi-group--jpa--escape-slashes-in-group-path` to `true` and then the slashes in the name are escaped with the character `~`. The escape char marks that the slash is part of the name and has no hierarchical meaning. The previous path example would be `/top/level1~/group` when escaped.
[source,bash]
----
bin/kc.[sh|bat] start --spi-group-jpa-escape-slashes-in-group-path=true
bin/kc.[sh|bat] start --spi-group--jpa--escape-slashes-in-group-path=true
----
The following example includes a top-level *Sales* group and a child *North America* subgroup.

View File

@ -32,12 +32,12 @@ To specify the lifespan override for offline user sessions, start {project_name}
[source,bash]
----
--spi-user-sessions-infinispan-offline-session-cache-entry-lifespan-override=<lifespan-in-seconds>
--spi-user-sessions--infinispan--offline-session-cache-entry-lifespan-override=<lifespan-in-seconds>
----
Similarly for offline client sessions:
[source,bash]
----
--spi-user-sessions-infinispan-offline-client-session-cache-entry-lifespan-override=<lifespan-in-seconds>
--spi-user-sessions--infinispan--offline-client-session-cache-entry-lifespan-override=<lifespan-in-seconds>
----

View File

@ -154,7 +154,7 @@ The CIBA grant uses the following two providers.
[source,bash,subs="attributes+"]
----
kc.[sh|bat] start --spi-ciba-auth-channel-ciba-http-auth-channel-http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path}
kc.[sh|bat] start --spi-ciba-auth-channel--ciba-http-auth-channel--http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path}
----
The configurable items and their description follow.

View File

@ -157,7 +157,7 @@ The CIBA grant uses the following two providers.
[source,bash,subs="attributes+"]
----
kc.[sh|bat] start --spi-ciba-auth-channel-ciba-http-auth-channel-http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path}
kc.[sh|bat] start --spi-ciba-auth-channel--ciba-http-auth-channel--http-authentication-channel-uri=https://backend.internal.example.com{kc_base_path}
----
The configurable items and their description follow.

View File

@ -25,7 +25,7 @@ The following example shows how to limit the number of active `AuthenticationSes
[source,bash]
----
bin/kc.[sh|bat] start --spi-authentication-sessions-infinispan-auth-sessions-limit=100
bin/kc.[sh|bat] start --spi-authentication-sessions--infinispan--auth-sessions-limit=100
----
ifeval::[{project_community}==true]
@ -33,6 +33,6 @@ The equivalent command for the new map storage:
[source,bash]
----
bin/kc.[sh|bat] start --spi-authentication-sessions-map-auth-sessions-limit=100
bin/kc.[sh|bat] start --spi-authentication-sessions--map--auth-sessions-limit=100
----
endif::[]

View File

@ -29,11 +29,11 @@ This is the list of the read-only attributes, which are used internally by the {
System administrators have a way to add additional attributes to this list. The configuration is currently available at the server level.
You can add this configuration by using the `spi-user-profile-declarative-user-profile-read-only-attributes` and `spi-user-profile-declarative-user-profile-admin-read-only-attributes` options. For example:
You can add this configuration by using the `spi-user-profile--declarative-user-profile--read-only-attributes` and `spi-user-profile--declarative-user-profile--admin-read-only-attributes` options. For example:
[source,bash,options="nowrap"]
----
kc.[sh|bat] start --spi-user-profile-declarative-user-profile-read-only-attributes=foo,bar*
kc.[sh|bat] start --spi-user-profile--declarative-user-profile--read-only-attributes=foo,bar*
----
For this example, users and administrators would not be able to update attribute `foo`. Users would not be able to edit any attributes starting with the `bar`.

View File

@ -33,7 +33,7 @@ All built-in providers support the configuration of key resolvers. A key resolve
[source,bash]
----
kc.[sh|bat] start --spi-vault-file-key-resolvers=REALM_UNDERSCORE_KEY,KEY_ONLY
kc.[sh|bat] start --spi-vault--file--key-resolvers=REALM_UNDERSCORE_KEY,KEY_ONLY
----
The resolvers run in the same order you declare them in the configuration. For each resolver, {project_name} uses the last entry name the resolver produces, which combines the realm with the vault key to search for the vault's secret. If {project_name} finds a secret, it returns the secret. If not, {project_name} uses the next resolver. This search continues until {project_name} finds a non-empty secret or runs out of resolvers. If {project_name} finds no secret, {project_name} returns an empty secret.

View File

@ -91,7 +91,7 @@ For example, to configure a provider you can set options as follows:
[source,bash]
----
bin/kc.[sh|bat] --spi-theme-selector-my-theme-selector-enabled=true --spi-theme-selector-my-theme-selector-theme=my-theme
bin/kc.[sh|bat] --spi-theme-selector--my-theme-selector--enabled=true --spi-theme-selector--my-theme-selector--theme=my-theme
----
Then you can retrieve the config in the `ProviderFactory` init method:
@ -234,7 +234,7 @@ one of them needs to be specified as the default one.
For example such as:
[source,bash]
----
bin/kc.[sh|bat] build --spi-hostname-provider=default
bin/kc.[sh|bat] build --spi-hostname--provider=default
----
The value `default` used as the value of `default-provider` must match the ID returned by the `ProviderFactory.getId()` of the particular provider factory implementation.
@ -293,7 +293,7 @@ For example to disable the Infinispan user cache provider use:
[source,bash]
----
bin/kc.[sh|bat] build --spi-user-cache-infinispan-enabled=false
bin/kc.[sh|bat] build --spi-user-cache--infinispan--enabled=false
----
[[_script_providers]]

View File

@ -32,13 +32,13 @@ NOTE: To set the theme for the `master` Admin Console you need to set the Admin
+
. To see the changes to the Admin Console refresh the page.
. Change the welcome theme by using the `spi-theme-welcome-theme` option.
. Change the welcome theme by using the `spi-theme--welcome-theme` option.
. For example:
+
[source,bash]
----
bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
bin/kc.[sh|bat] start --spi-theme--welcome-theme=custom-theme
----
[[_default-themes]]
@ -74,7 +74,7 @@ restarting {project_name}.
+
[source,bash]
----
bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
bin/kc.[sh|bat] start --spi-theme--static-max-age=-1 --spi-theme--cache-themes=false --spi-theme-cache--templates=false
----
. Create a directory in the `themes` directory.

View File

@ -213,7 +213,7 @@ For example, by running the server with the following argument:
[source,bash]
----
kc.[sh|bat] start --spi-storage-readonly-property-file-path=/other-users.properties
kc.[sh|bat] start --spi-storage--readonly-property-file--path=/other-users.properties
----
We can specify the classpath of the user property file instead of hardcoding it. Then you can retrieve the configuration in the `PropertyFileUserStorageProviderFactory.init()`:

View File

@ -3,6 +3,18 @@
Breaking changes are identified as requiring changes from existing users to their configurations.
In minor or patch releases we will only do breaking changes to fix bugs.
=== Buildtime SPI Options
SPI options ending in `-enabled`, `-provider-default`, or `-provider` were previously treated as buildtime. However in some instances this was not correct as a provider could have a configuration property ending in one of those suffixes as well.
To resolve this ambiguity, and any potential ambiguity involving SPI and provider names, a new SPI option format was introduced where the scopes and suffix are separated by `--`(double dash) instead of `-`(dash).
An SPI property ending in `-enabled`, `-provider-default`, or `-provider` will only be treated as buildtime if it is using the new format, e.g. `spi-<spi-name>--<provider-name>--enabled` will be recognized as a buildtime option.
For instance, the correct way to reference your custom email template is: `--spi-email-template--mycustomprovider--enabled` (not `--spi-email-template-mycustomprovider-enabled`).
Options using the legacy format and ending in `-enabled`, `-provider-default`, or `-provider` will instead be treated as runtime and a warning will be emitted.
=== Reading information about temporarily locked users
In previous releases there was an inconsistency in the REST endpoint result of getting a user (`+GET /admin/realms/{realm}/users/{user-id}+`) and searching for a user (`+GET /admin/realms/{realm}/users+`). When BruteForce is enabled and a user was temporarily locked out the former endpoint would return `enabled=false` while the latter would return `enabled=true`. If the user was updated and enabled was false due to temporary lockout then the user would be disabled permanently. Both endpoints now return `enabled=true` when a user is temporarily locked out. To check whether a user is temporarily locked out the BruteForceUserResource endpoint should be utilised (`+GET /admin/realms/{realm}/attack-detection/brute-force/users/{userId}+`).
@ -137,6 +149,10 @@ In relation to supported Recovery codes, we deprecated the password policy `Reco
This password policy was not related to passwords at all, but was related to recovery codes, and hence using password policy is not appropriate way for the configuration of the threshold. It is
recommended to use the configuration option *Warning Threshold* of the *Recovery Authentication Codes* required action instead of using password policy. For more details, see the link:{adminguide_link}#_recovery-codes[Recovery codes documentation].
=== Scope.getPropertyNames deprecated for removal
The `org.keycloak.Config.Scope.getPropertyNames` method has been deprecated for removal.
== Removed features
The following features have been removed from this release.

View File

@ -25,7 +25,7 @@ To change the threshold, set the `index-creation-threshold` property, value for
[source,bash]
----
kc.[sh|bat] start --spi-connections-liquibase-quarkus-index-creation-threshold=300000
kc.[sh|bat] start --spi-connections-liquibase--quarkus--index-creation-threshold=300000
----
=== Manual relational database migration
@ -35,7 +35,7 @@ default `connections-jpa` provider:
[source,bash]
----
kc.[sh|bat] start --spi-connections-jpa-quarkus-migration-strategy=manual
kc.[sh|bat] start --spi-connections-jpa--quarkus--migration-strategy=manual
----
When you start the server with this configuration, the server checks if the database needs to be migrated. If migration is needed, the required changes are written to the `bin/keycloak-database-update.sql` SQL file. You can review and manually run these commands against the database.
@ -45,7 +45,7 @@ default `connections-jpa` provider:
[source,bash]
----
kc.[sh|bat] start --spi-connections-jpa-quarkus-migration-export=<path>/<file.sql>
kc.[sh|bat] start --spi-connections-jpa--quarkus--migration-export=<path>/<file.sql>
----
For further details on how to apply this file to the database, see the documentation for your relational database.

View File

@ -73,7 +73,7 @@ Possible values:
--
====
To configure what tags are available provide a comma-separated list of tag names to the following option `spi-credential-keycloak-password-validations-counter-tags`.
To configure what tags are available provide a comma-separated list of tag names to the following option `spi-credential--keycloak-password--validations-counter-tags`.
By default, all tags are enabled.
The snippet below is an example of a response provided by the metric endpoint:

View File

@ -91,11 +91,11 @@ metadata:
spec:
...
additionalOptions:
- name: spi-connections-http-client-default-connection-pool-size
- name: spi-connections-http-client--default--connection-pool-size
secret: # Secret reference
name: http-client-secret # name of the Secret
key: poolSize # name of the Key in the Secret
- name: spi-email-template-mycustomprovider-enabled
- name: spi-email-template--mycustomprovider--enabled
value: true # plain text value
----
NOTE: The name format of options defined in this way is identical to the key format of options specified in the configuration file.

View File

@ -19,6 +19,12 @@ any provider, including those you have implemented to extend the server capabili
Providers can be configured by using a specific configuration format. The format consists of:
[source]
----
spi-<spi-id>--<provider-id>--<property>=<value>
----
Or if there is no possibility of ambiguity between multiple providers:
----
spi-<spi-id>-<provider-id>-<property>=<value>
----
@ -27,7 +33,9 @@ The `<spi-id>` is the name of the SPI you want to configure.
The `<provider-id>` is the id of the provider you want to configure. This is the id set to the corresponding provider factory implementation.
The `<property>` is the actual name of the property you want to set for a given provider.
The `<property>` is the actual name of the property you want to set for a given provider
NOTE: the property name `enabled` is effectively reserved for enabling / disabling a provider
All those names (for spi, provider, and property) should be in lower case and if the name is in camel-case such as `myKeycloakProvider`, it should include dashes (`-`) before upper-case letters as follows: `my-keycloak-provider`.
@ -35,31 +43,47 @@ Taking the `HttpClientSpi` SPI as an example, the name of the SPI is `connection
[source]
----
spi-connections-http-client-default-connection-pool-size=10
spi-connections-http-client--default--connection-pool-size=10
----
== Setting a provider configuration option
=== Setting a provider configuration option
Provider configuration options are provided when starting the server. See all support configuration sources and formats for options in <@links.server id="configuration"/>. For example via a command line option:
.Setting the `connection-pool-size` for the `default` provider of the `connections-http-client` SPI
<@kc.start parameters="--spi-connections-http-client-default-connection-pool-size=10"/>
<@kc.start parameters="--spi-connections-http-client--default--connection-pool-size=10"/>
== Configuring a single provider for an SPI
== Build time options
=== Configuring a single provider for an SPI
Depending on the SPI, multiple provider implementations can co-exist but only one of them is going to be used at runtime.
For these SPIs, a specific provider is the primary implementation that is going to be active and used at runtime.
For these SPIs, a specific provider is the primary implementation that is going to be active and used at runtime. The format consists of:
[source]
----
spi-<spi-id>--provider=<provider-id>
----
NOTE: `spi-<spi-id>-provider=<provider-id>` may still be used, but the server will not properly detect when reaugmentation is needed.
To configure a provider as the single provider you should run the `build` command as follows:
.Marking the `mycustomprovider` provider as the single provider for the `email-template` SPI
<@kc.build parameters="--spi-email-template-provider=mycustomprovider"/>
<@kc.build parameters="--spi-email-template--provider=mycustomprovider"/>
== Configuring a default provider for an SPI
=== Configuring a default provider for an SPI
Depending on the SPI, multiple provider implementations can co-exist and one is used by default.
For these SPIs, a specific provider is the default implementation that is going to selected unless a specific provider
is requested.
is requested. The format consists of:
[source]
----
spi-<spi-id>--provider-default=<provider-id>
----
NOTE: `spi-<spi-id>-provider-default=<provider-id>` may still be used, but the server will not properly detect when reaugmentation is needed.
The following logic is used to determine the default provider:
@ -70,15 +94,23 @@ The following logic is used to determine the default provider:
To configure a provider as the default provider you should run the `build` command as follows:
.Marking the `mycustomhash` provider as the default provider for the `password-hashing` SPI
<@kc.build parameters="--spi-password-hashing-provider-default=mycustomprovider"/>
<@kc.build parameters="--spi-password-hashing--provider-default=mycustomprovider"/>
=== Enabling and disabling a provider
== Enabling and disabling a provider
The format consists of:
[source]
----
spi-<spi-id>--<provider-id>--enabled=<boolean>
----
NOTE: `spi-<spi-id>-<provider-id>-enabled=<boolean>` may still be used, but the server will not properly detect when reaugmentation is needed.
To enable or disable a provider you should run the `build` command as follows:
.Enabling a provider
<@kc.build parameters="--spi-email-template-mycustomprovider-enabled=true"/>
<@kc.build parameters="--spi-email-template--mycustomprovider--enabled=true"/>
To disable a provider, use the same command and set the `enabled` property to `false`.

View File

@ -293,12 +293,12 @@ You can achieve most optimizations to startup and runtime behavior by using the
Some of the realm capabilities allow administrators to reference system variables such as environment variables and system properties when configuring
the realm and its components.
By default, {project_name} disallow using system variables but only those explicitly specified through the `spi-admin-allowed-system-variables` configuration
By default, {project_name} disallow using system variables but only those explicitly specified through the `+spi-admin--allowed-system-variables+` configuration
option. This option allows you to specify a comma-separated list of keys that will eventually resolve to values from system variables with the same key.
. Start the server and expose a set of system variables to the server runtime
+
<@kc.start parameters="--spi-admin-allowed-system-variables=FOO,BAR"/>
<@kc.start parameters="--spi-admin--allowed-system-variables=FOO,BAR"/>
In future releases, this capability will be removed in favor of preventing any usage of system variables in the realm configuration.

View File

@ -308,7 +308,7 @@ Because cluster nodes can boot concurrently, they take extra time for database a
The maximum timeout for this lock is 900 seconds. If a node waits on this lock for more than the timeout, the boot fails. The need to change the default value is unlikely, but you can change it by entering this command:
<@kc.start parameters="--spi-dblock-jpa-lock-wait-timeout 900"/>
<@kc.start parameters="--spi-dblock--jpa--lock-wait-timeout 900"/>
== Using Database Vendors with XA transaction support
{project_name} uses non-XA transactions and the appropriate database drivers by default.
@ -330,17 +330,17 @@ NOTE: Enabling XA transactions in a containerized environment does not fully sup
To setup the JPA migrationStrategy (manual/update/validate) you should setup JPA provider as follows:
.Setting the `migration-strategy` for the `quarkus` provider of the `connections-jpa` SPI
<@kc.start parameters="--spi-connections-jpa-quarkus-migration-strategy=manual"/>
<@kc.start parameters="--spi-connections--jpa--quarkus-migration-strategy=manual"/>
If you want to get a SQL file for DB initialization, too, you have to add this additional SPI initializeEmpty (true/false):
.Setting the `initialize-empty` for the `quarkus` provider of the `connections-jpa` SPI
<@kc.start parameters="--spi-connections-jpa-quarkus-initialize-empty=false"/>
<@kc.start parameters="--spi-connections--jpa--quarkus-initialize-empty=false"/>
In the same way the migrationExport to point to a specific file and location:
.Setting the `migration-export` for the `quarkus` provider of the `connections-jpa` SPI
<@kc.start parameters="--spi-connections-jpa-quarkus-migration-export=<path>/<file.sql>"/>
<@kc.start parameters="--spi-connections--jpa--quarkus-migration-export=<path>/<file.sql>"/>
For more information, check the link:{upgrading_guide_link}#_migrate_db[Migrating the database] documentation.

View File

@ -138,14 +138,14 @@ So effectively, you can use an option such as the following when starting the se
[source]
----
--spi-password-hashing-pbkdf2-sha512-max-padding-length=14
--spi-password-hashing--pbkdf2-sha512--max-padding-length=14
----
NOTE: Using the option above does not break FIPS compliance. However, note that longer passwords are good practice anyway. For example, passwords auto-generated by modern browsers match this
requirement as they are longer than 14 characters. If you want to omit the option for max-padding-length, you can set the password policy to your realms to have passwords at least 14 characters long.
NOTE: When you are migrating from {project_name} older than 24, or if you explicitly set the password policy to override the default hashing algorithm, it is possible that some of your users use an older
algorithm like `pbkdf2-sha256`. In this case, consider adding the `--spi-password-hashing-pbkdf2-sha256-max-padding-length=14` option to ensure that users having their passwords hashed with
algorithm like `pbkdf2-sha256`. In this case, consider adding the `+--spi-password-hashing--pbkdf2-sha256--max-padding-length=14+` option to ensure that users having their passwords hashed with
the older `pbkdf2-sha256` can log in because their passwords may be shorter than 14 characters.
* RSA keys of 1024 bits do not work (2048 is the minimum). This applies for keys used by the {project_name} realm itself (Realm keys from the `Keys` tab in the admin console), but also client keys and IDP keys

View File

@ -17,7 +17,7 @@ to configure a {project_name} Truststore so that {project_name} is able to perfo
== Client Configuration Command
The HTTP client that {project_name} uses for outgoing communication is highly configurable. To configure the {project_name} outgoing HTTP client, enter this command:
<@kc.start parameters="--spi-connections-http-client-default-<configurationoption>=<value>"/>
<@kc.start parameters="--spi-connections-http-client--default--<configurationoption>=<value>"/>
The following are the command options:
@ -90,7 +90,7 @@ For example, consider the following regex:
You apply a regex-based hostname pattern by entering this command:
<@kc.start parameters="--spi-connections-http-client-default-proxy-mappings=\'.*\\\\.(google|googleapis)\\\\.com;http://www-proxy.acme.com:8080\'"/>
<@kc.start parameters="--spi-connections-http-client--default--proxy-mappings=\'.*\\\\.(google|googleapis)\\\\.com;http://www-proxy.acme.com:8080\'"/>
The backslash character `\` is escaped again because micro-profile config is used to parse the array of mappings.

View File

@ -84,12 +84,12 @@ From this point, it is beneficial if load balancer forwards all the next request
The sticky session is not mandatory for the cluster setup, however it is good for performance for the reasons mentioned above. You need to configure your loadbalancer to stick over the AUTH_SESSION_ID cookie. The appropriate procedure to make this change depends on your loadbalancer.
If your proxy supports session affinity without processing cookies from backend nodes, you should set the `spi-sticky-session-encoder-infinispan-should-attach-route` option
If your proxy supports session affinity without processing cookies from backend nodes, you should set the `+spi-sticky-session-encoder--infinispan--should-attach-route+` option
to `false` in order to avoid attaching the node to cookies and just rely on the reverse proxy capabilities.
<@kc.start parameters="--spi-sticky-session-encoder-infinispan-should-attach-route=false"/>
<@kc.start parameters="--spi-sticky-session-encoder--infinispan--should-attach-route=false"/>
By default, the `spi-sticky-session-encoder-infinispan-should-attach-route` option value is `true` so that the node name is attached to
By default, the `+spi-sticky-session-encoder--infinispan--should-attach-route+` option value is `true` so that the node name is attached to
cookies to indicate to the reverse proxy the node that subsequent requests should be sent to.
== Exposed path recommendations
@ -169,9 +169,9 @@ Client certificate lookup via a proxy header for X.509 authentication is conside
and edge TLS termination.
* If passthrough is not an option, implement the following security measures:
** Configure your network so that {project_name} is isolated and can accept connections only from the proxy.
** Make sure that the proxy overwrites the header that is configured in `spi-x509cert-lookup-<provider>-ssl-client-cert` option.
** Pay extra attention to the `spi-x509cert-lookup-<provider>-trust-proxy-verification` setting. Make sure you enable it only if you can trust your proxy to verify the client certificate.
Setting `spi-x509cert-lookup-<provider>-trust-proxy-verification=true` without the proxy verifying the client certificate chain will expose {project_name} to security vulnerability
** Make sure that the proxy overwrites the header that is configured in `spi-x509cert-lookup--<provider>--ssl-client-cert` option.
** Pay extra attention to the `spi-x509cert-lookup--<provider>--trust-proxy-verification` setting. Make sure you enable it only if you can trust your proxy to verify the client certificate.
Setting `spi-x509cert-lookup--<provider>--trust-proxy-verification=true` without the proxy verifying the client certificate chain will expose {project_name} to security vulnerability
when a forged client certificate can be used for authentication.
====
@ -194,10 +194,10 @@ The server supports some of the most commons TLS termination proxies such as:
To configure how client certificates are retrieved from the requests you need to:
.Enable the corresponding proxy provider
<@kc.build parameters="--spi-x509cert-lookup-provider=<provider>"/>
<@kc.build parameters="--spi-x509cert-lookup--provider=<provider>"/>
.Configure the HTTP headers
<@kc.start parameters="--spi-x509cert-lookup-<provider>-ssl-client-cert=SSL_CLIENT_CERT --spi-x509cert-lookup-<provider>-ssl-cert-chain-prefix=CERT_CHAIN --spi-x509cert-lookup-<provider>-certificate-chain-length=10"/>
<@kc.start parameters="--spi-x509cert-lookup--<provider>--ssl-client-cert=SSL_CLIENT_CERT --spi-x509cert-lookup--<provider>--ssl-cert-chain-prefix=CERT_CHAIN --spi-x509cert-lookup--<provider>-certificate-chain-length=10"/>
When configuring the HTTP headers, you need to make sure the values you are using correspond to the name of the headers
forwarded by the proxy with the client certificate information.

View File

@ -23,7 +23,7 @@
<#list options as option>
|
[.options-key]#``${option.key}``# <#if buildIcon><#if option.build>[.none]#icon:tools[role=options-build]#</#if></#if>
[.options-key]#`+${option.key}+`# <#if buildIcon><#if option.build>[.none]#icon:tools[role=options-build]#</#if></#if>
[.options-description]#${option.description}#
@ -31,8 +31,8 @@
--
<#if option.descriptionExtended?has_content>[.options-description-extended]#${option.descriptionExtended!}#</#if>
*CLI:* `${option.keyCli}` +
*Env:* `${option.keyEnv}`
*CLI:* `+${option.keyCli}+` +
*Env:* `+${option.keyEnv}+`
--
<#if option.enabledWhen?has_content>

View File

@ -36,13 +36,13 @@ NOTE: To set the theme for the `master` Admin Console you need to set the Admin
+
. To see the changes to the Admin Console refresh the page.
. Change the welcome theme by using the `spi-theme-welcome-theme` option.
. Change the welcome theme by using the `spi-theme--welcome-theme` option.
. For example:
+
[source,bash]
----
bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
bin/kc.[sh|bat] start --spi-theme--welcome-theme=custom-theme
----
[[_default-themes]]
@ -79,7 +79,7 @@ restarting {project_name}.
+
[source,bash]
----
bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme-cache-templates=false
bin/kc.[sh|bat] start --spi-theme-static-max-age=-1 --spi-theme-cache-themes=false --spi-theme--cache-templates=false
----
. Create a directory in the `themes` directory.

View File

@ -12,9 +12,9 @@ By default, the welcome theme is only used to create the initial temporary admin
Since the welcome theme is not associated with a realm, it cannot be selected in the admin console like other themes.
To change the welcome theme, create and deploy a new welcome theme as described in <<_creating-a-theme,Creating a theme>>. Then, start the {project_name} server using the `spi-theme-welcome-theme` option.
To change the welcome theme, create and deploy a new welcome theme as described in <<_creating-a-theme,Creating a theme>>. Then, start the {project_name} server using the `spi-theme--welcome-theme` option.
[source,bash]
----
bin/kc.[sh|bat] start --spi-theme-welcome-theme=custom-theme
bin/kc.[sh|bat] start --spi-theme--welcome-theme=custom-theme
----
</@tmpl.guide>

View File

@ -1,11 +1,9 @@
package org.keycloak.guides.maven;
import static org.aesh.readline.terminal.Key.r;
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
import static org.keycloak.quarkus.runtime.configuration.Configuration.toDashCase;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import org.apache.commons.lang3.ArrayUtils;
import org.keycloak.config.ConfigSupportLevel;
import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.OptionCategory;
@ -14,7 +12,6 @@ import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.Spi;
import org.keycloak.quarkus.runtime.Providers;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.utils.StringUtil;
@ -63,6 +60,10 @@ public class Options {
.flatMap(Collection::stream)
.forEach(option -> option.description = option.description.replaceAll("'([^ ]*)'", "`$1`"));
ArrayList<String> booleanValues = new ArrayList<>();
booleanValues.add("true");
booleanValues.add("false");
for (Spi loadSpi : providerManager.loadSpis().stream().sorted(Comparator.comparing(Spi::getName)).toList()) {
for (ProviderFactory<?> providerFactory : providerManager.load(loadSpi).stream().sorted(Comparator.comparing(ProviderFactory::getId)).toList()) {
List<ProviderConfigProperty> configMetadata = providerFactory.getConfigMetadata();
@ -71,9 +72,11 @@ public class Options {
continue;
}
String optionPrefix = NS_KEYCLOAK_PREFIX + String.join(OPTION_PART_SEPARATOR, ArrayUtils.insert(0, new String[] {loadSpi.getName(), providerFactory.getId()}, "spi"));
String spiKey = toDashCase(loadSpi.getName());
String providerKey = toDashCase(providerFactory.getId());
String optionPrefix = NS_KEYCLOAK_PREFIX + "spi" + OPTION_PART_SEPARATOR + spiKey + OPTION_PART_SEPARATOR + OPTION_PART_SEPARATOR + providerKey + OPTION_PART_SEPARATOR + OPTION_PART_SEPARATOR;
List<Option> options = configMetadata.stream()
.map(m -> new Option(Configuration.toDashCase(optionPrefix.concat("-") + m.getName()), OptionCategory.GENERAL, false,
.map(m -> new Option(optionPrefix + toDashCase(m.getName()), OptionCategory.GENERAL, false,
m.getType(),
m.getHelpText(),
m.getDefaultValue() == null ? null : m.getDefaultValue().toString(),
@ -83,9 +86,6 @@ public class Options {
null))
.sorted(Comparator.comparing(Option::getKey)).collect(Collectors.toList());
ArrayList<String> booleanValues = new ArrayList<>();
booleanValues.add("true");
booleanValues.add("false");
options.forEach(option -> {
if (option.type.equals("boolean")) {
option.expectedValues = booleanValues;
@ -94,7 +94,7 @@ public class Options {
});
if (!options.isEmpty()) {
providerOptions.computeIfAbsent(toDashCase(loadSpi.getName()), k -> new LinkedHashMap<>()).put(toDashCase(providerFactory.getId()), options);
providerOptions.computeIfAbsent(spiKey, k -> new LinkedHashMap<>()).put(providerKey, options);
}
}
}
@ -261,7 +261,9 @@ public class Options {
}
public String getEnabledWhen() {
if (StringUtil.isBlank(enabledWhen)) return null;
if (StringUtil.isBlank(enabledWhen)) {
return null;
}
return enabledWhen;
}

View File

@ -38,6 +38,7 @@ import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -377,10 +378,11 @@ public class Picocli {
DisabledMappersInterceptor.disable(); // we want all properties, even disabled ones
final List<String> ignoredRunTime = new ArrayList<>();
final Set<String> disabledBuildTime = new HashSet<>();
final Set<String> disabledRunTime = new HashSet<>();
final Set<String> deprecatedInUse = new HashSet<>();
final Set<String> missingOption = new HashSet<>();
final Set<String> disabledBuildTime = new LinkedHashSet<>();
final Set<String> disabledRunTime = new LinkedHashSet<>();
final Set<String> deprecatedInUse = new LinkedHashSet<>();
final Set<String> missingOption = new LinkedHashSet<>();
final Set<String> ambiguousSpi = new LinkedHashSet<>();
final LinkedHashMap<String, String> secondClassOptions = new LinkedHashMap<>();
final Set<PropertyMapper<?>> disabledMappers = new HashSet<>();
@ -402,6 +404,9 @@ public class Picocli {
if (!options.includeRuntime) {
checkRuntimeSpiOptions(name, ignoredRunTime);
}
if (PropertyMappers.isMaybeSpiBuildTimeProperty(name)) {
ambiguousSpi.add(name);
}
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
if (mapper == null) {
return; // TODO: need to look for disabled Wildcard mappers
@ -460,7 +465,9 @@ public class Picocli {
if (!deprecatedInUse.isEmpty()) {
warn("The following used options or option values are DEPRECATED and will be removed or their behaviour changed in a future release:\n" + String.join("\n", deprecatedInUse) + "\nConsult the Release Notes for details.", getOutWriter());
}
if (!ambiguousSpi.isEmpty()) {
warn("The following spi options are using the legacy format and are not being treated as build time options. Please use the new format with the appropriate -- separators to resolve this ambiguity: " + String.join("\n", ambiguousSpi));
}
secondClassOptions.forEach((key, firstClass) -> {
warn("Please use the first-class option `%s` instead of `%s`".formatted(firstClass, key), getOutWriter());
});

View File

@ -176,11 +176,17 @@ public final class Configuration {
}
public static String toDashCase(String key) {
if (key == null) {
return null;
}
StringBuilder sb = new StringBuilder(key.length());
boolean l = false;
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (c == ',') {
c = '-'; // should not happen, but was allowed by the previous logic
}
if (l && Character.isUpperCase(c)) {
sb.append('-');
c = Character.toLowerCase(c);

View File

@ -19,12 +19,10 @@ package org.keycloak.quarkus.runtime.configuration;
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
import static org.keycloak.quarkus.runtime.configuration.Configuration.toDashCase;
import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarFormat;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.microprofile.config.ConfigProvider;
import org.keycloak.Config;
@ -33,6 +31,7 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider {
public static final String NS_KEYCLOAK = "kc";
public static final String NS_KEYCLOAK_PREFIX = NS_KEYCLOAK + ".";
public static final String SPI_PREFIX = NS_KEYCLOAK_PREFIX + "spi" + OPTION_PART_SEPARATOR;
public static final String NS_QUARKUS = "quarkus";
public static final String NS_QUARKUS_PREFIX = "quarkus" + ".";
@ -58,21 +57,21 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider {
@Override
public Config.Scope scope(String... scope) {
return new MicroProfileScope(scope);
return new MicroProfileScope(SPI_PREFIX, scope);
}
public class MicroProfileScope implements Config.Scope {
private final String[] scope;
private final String prefix;
private final String separatorPrefix;
public MicroProfileScope(String... scopes) {
this.scope = scopes;
StringBuilder prefix = new StringBuilder(NS_KEYCLOAK_PREFIX).append("spi");
public MicroProfileScope(String prefix, String... scopes) {
StringBuilder prefixBuilder = new StringBuilder(prefix);
for (String scope : scopes) {
prefix.append(OPTION_PART_SEPARATOR).append(scope);
prefixBuilder.append(toDashCase(scope)).append(OPTION_PART_SEPARATOR + OPTION_PART_SEPARATOR);
}
this.prefix = prefix.toString();
this.separatorPrefix = prefixBuilder.toString();
this.prefix = separatorPrefix.replace(OPTION_PART_SEPARATOR + OPTION_PART_SEPARATOR, OPTION_PART_SEPARATOR);
}
@Override
@ -122,23 +121,22 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider {
@Override
public Config.Scope scope(String... scope) {
return new MicroProfileScope(ArrayUtils.addAll(this.scope, scope));
return new MicroProfileScope(prefix, scope);
}
@Override
public Set<String> getPropertyNames() {
return StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(this::startWithPrefix)
.filter(key -> key.startsWith(separatorPrefix))
.collect(Collectors.toSet());
}
private <T> T getValue(String key, Class<T> clazz, T defaultValue) {
return config.getOptionalValue(toDashCase(prefix.concat(OPTION_PART_SEPARATOR).concat(key.replace('.', '-'))), clazz).orElse(defaultValue);
String dashCase = toDashCase(key);
return config.getOptionalValue(separatorPrefix.concat(dashCase), clazz)
.or(() -> config.getOptionalValue(prefix.concat(dashCase), clazz)).orElse(defaultValue);
}
private boolean startWithPrefix(String key) {
return key.startsWith(toDashCase(prefix)) || key.startsWith(toDashCase(toEnvVarFormat(prefix)));
}
}
}

View File

@ -40,7 +40,7 @@ final class CachingPropertyMappers {
.build(),
fromOption(CachingOptions.CACHE_STACK)
.isEnabled(CachingPropertyMappers::cacheSetToInfinispan, CACHE_STACK_SET_TO_ISPN)
.to("kc.spi-cache-embedded-default-stack")
.to("kc.spi-cache--embedded--default-stack")
.paramLabel("stack")
.build(),
fromOption(CachingOptions.CACHE_CONFIG_FILE)
@ -49,10 +49,11 @@ final class CachingPropertyMappers {
return "cache-local.xml";
} else if (CachingOptions.Mechanism.ispn.name().equals(value)) {
return resolveConfigFile("cache-ispn.xml", null);
} else
} else {
return null;
}
})
.to("kc.spi-cache-embedded-default-config-file")
.to("kc.spi-cache-embedded--default--config-file")
.transformer(CachingPropertyMappers::resolveConfigFile)
.validator(s -> {
if (!Files.exists(Paths.get(resolveConfigFile(s, null)))) {
@ -62,72 +63,72 @@ final class CachingPropertyMappers {
.paramLabel("file")
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED)
.to("kc.spi-jgroups-mtls-default-enabled")
.to("kc.spi-jgroups-mtls--default--enabled")
.isEnabled(CachingPropertyMappers::getDefaultMtlsEnabled, "a TCP based cache-stack is used")
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE.withRuntimeSpecificDefault(getDefaultKeystorePathValue()))
.paramLabel("file")
.to("kc.spi-jgroups-mtls-default-keystore-file")
.to("kc.spi-jgroups-mtls--default--keystore-file")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkValidKeystore(value, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD)
.paramLabel("password")
.isMasked(true)
.to("kc.spi-jgroups-mtls-default-keystore-password")
.to("kc.spi-jgroups-mtls--default--keystore-password")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_KEYSTORE))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE.withRuntimeSpecificDefault(getDefaultTruststorePathValue()))
.paramLabel("file")
.to("kc.spi-jgroups-mtls-default-truststore-file")
.to("kc.spi-jgroups-mtls--default--truststore-file")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkValidKeystore(value, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD)
.paramLabel("password")
.isMasked(true)
.to("kc.spi-jgroups-mtls-default-truststore-password")
.to("kc.spi-jgroups-mtls--default--truststore-password")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(value -> checkOptionPresent(CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD, CachingOptions.CACHE_EMBEDDED_MTLS_TRUSTSTORE))
.build(),
fromOption(CachingOptions.CACHE_EMBEDDED_MTLS_ROTATION)
.paramLabel("days")
.to("kc.spi-jgroups-mtls-default-rotation")
.to("kc.spi-jgroups-mtls--default--rotation")
.isEnabled(() -> Configuration.isTrue(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED), "property '%s' is enabled".formatted(CachingOptions.CACHE_EMBEDDED_MTLS_ENABLED.getKey()))
.validator(CachingPropertyMappers::validateCertificateRotationIsPositive)
.build(),
fromOption(CachingOptions.CACHE_REMOTE_HOST)
.paramLabel("hostname")
.to("kc.spi-cache-remote-default-hostname")
.to("kc.spi-cache-remote--default--hostname")
.addValidateEnabled(CachingPropertyMappers::isRemoteCacheHostEnabled, MULTI_SITE_OR_EMBEDDED_REMOTE_FEATURE_SET)
.isRequired(InfinispanUtils::isRemoteInfinispan, MULTI_SITE_FEATURE_SET)
.build(),
fromOption(CachingOptions.CACHE_REMOTE_PORT)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-port")
.to("kc.spi-cache-remote--default--port")
.paramLabel("port")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_TLS_ENABLED)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-tls-enabled")
.to("kc.spi-cache-remote--default--tls-enabled")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_USERNAME)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-username")
.to("kc.spi-cache-remote--default--username")
.validator((value) -> validateCachingOptionIsPresent(CachingOptions.CACHE_REMOTE_USERNAME, CachingOptions.CACHE_REMOTE_PASSWORD))
.paramLabel("username")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_PASSWORD)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-password")
.to("kc.spi-cache-remote--default--password")
.validator((value) -> validateCachingOptionIsPresent(CachingOptions.CACHE_REMOTE_PASSWORD, CachingOptions.CACHE_REMOTE_USERNAME))
.paramLabel("password")
.isMasked(true)
.build(),
fromOption(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)
.isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG)
.to("kc.spi-cache-embedded-default-metrics-histograms-enabled")
.to("kc.spi-cache-embedded--default--metrics-histograms-enabled")
.build()
);
@ -135,11 +136,13 @@ final class CachingPropertyMappers {
List<PropertyMapper<?>> mappers = new ArrayList<>(numMappers);
mappers.addAll(staticMappers);
for (String cache : CachingOptions.LOCAL_MAX_COUNT_CACHES)
for (String cache : CachingOptions.LOCAL_MAX_COUNT_CACHES) {
mappers.add(maxCountOpt(cache, () -> true, ""));
}
for (String cache : CachingOptions.CLUSTERED_MAX_COUNT_CACHES)
for (String cache : CachingOptions.CLUSTERED_MAX_COUNT_CACHES) {
mappers.add(maxCountOpt(cache, InfinispanUtils::isEmbeddedInfinispan, "embedded Infinispan clusters configured"));
}
return mappers.toArray(new PropertyMapper[0]);
}
@ -212,7 +215,7 @@ final class CachingPropertyMappers {
return fromOption(CachingOptions.maxCountOption(cacheName))
.isEnabled(isEnabled, enabledWhen)
.paramLabel("max-count")
.to("kc.spi-cache-embedded-default-%s-max-count".formatted(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, cacheName)))
.to("kc.spi-cache-embedded--default--%s-max-count".formatted(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, cacheName)))
.build();
}
@ -228,8 +231,9 @@ final class CachingPropertyMappers {
private static void checkValidKeystore(String store, Option<String> option, Option<String> requiredOption) {
checkOptionPresent(option, requiredOption);
if (!new File(store).exists())
if (!new File(store).exists()) {
throw new IllegalArgumentException("The '%s' file '%s' does not exist.".formatted(option.getKey(), store));
}
}
private static void checkOptionPresent(Option<String> option, Option<String> requiredOption) {

View File

@ -18,16 +18,16 @@ final class EventPropertyMappers {
public static PropertyMapper<?>[] getMetricsPropertyMappers() {
return new PropertyMapper[] {
fromOption(USER_EVENT_METRICS_ENABLED)
.to("kc.spi-events-listener-micrometer-user-event-metrics-enabled")
.to("kc.spi-events-listener--micrometer-user-event-metrics--enabled")
.isEnabled(EventPropertyMappers::userEventsMetricsEnabled, METRICS_ENABLED_MSG + " and feature " + Profile.Feature.USER_EVENT_METRICS.getKey() + " is enabled")
.build(),
fromOption(USER_EVENT_METRICS_TAGS)
.to("kc.spi-events-listener-micrometer-user-event-metrics-tags")
.to("kc.spi-events-listener--micrometer-user-event-metrics--tags")
.paramLabel("tags")
.isEnabled(EventPropertyMappers::userEventsMetricsTags, "user event metrics are enabled")
.build(),
fromOption(USER_EVENT_METRICS_EVENTS)
.to("kc.spi-events-listener-micrometer-user-event-metrics-events")
.to("kc.spi-events-listener--micrometer-user-event-metrics--events")
.paramLabel("events")
.isEnabled(EventPropertyMappers::userEventsMetricsTags, "user event metrics are enabled")
.build(),

View File

@ -33,7 +33,7 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.isBlank;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
public final class ExportPropertyMappers {
private static final String EXPORTER_PROPERTY = "kc.spi-export-exporter";
private static final String EXPORTER_PROPERTY = "kc.spi-export--exporter";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
@ -48,30 +48,30 @@ public final class ExportPropertyMappers {
.paramLabel("file")
.build(),
fromOption(ExportOptions.FILE)
.to("kc.spi-export-single-file-file")
.to("kc.spi-export--single-file--file")
.paramLabel("file")
.build(),
fromOption(ExportOptions.DIR)
.to("kc.spi-export-dir-dir")
.to("kc.spi-export--dir--dir")
.paramLabel("dir")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-single-file-realm-name")
.to("kc.spi-export--single-file--realm-name")
.isEnabled(ExportPropertyMappers::isSingleFileProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.REALM)
.to("kc.spi-export-dir-realm-name")
.to("kc.spi-export--dir--realm-name")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("realm")
.build(),
fromOption(ExportOptions.USERS)
.to("kc.spi-export-dir-users-export-strategy")
.to("kc.spi-export--dir--users-export-strategy")
.addValidator(ExportPropertyMappers::validateUsersUsage)
.paramLabel("strategy")
.build(),
fromOption(ExportOptions.USERS_PER_FILE)
.to("kc.spi-export-dir-users-per-file")
.to("kc.spi-export--dir--users-per-file")
.isEnabled(ExportPropertyMappers::isDirProvider)
.paramLabel("number")
.build()
@ -121,8 +121,8 @@ public final class ExportPropertyMappers {
return exporter.getValue();
}
var file = Configuration.getOptionalValue("kc.spi-export-single-file-file").map(f -> SINGLE_FILE);
var dir = Configuration.getOptionalValue("kc.spi-export-dir-dir")
var file = Configuration.getOptionalValue("kc.spi-export--single-file--file").map(f -> SINGLE_FILE);
var dir = Configuration.getOptionalValue("kc.spi-export--dir--dir")
.or(() -> Configuration.getOptionalValue("kc.dir"))
.map(f -> DIR);

View File

@ -27,15 +27,15 @@ public final class HostnameV2PropertyMappers {
public static PropertyMapper<?>[] getHostnamePropertyMappers() {
return Stream.of(
fromOption(HostnameV2Options.HOSTNAME)
.to("kc.spi-hostname-v2-hostname")
.to("kc.spi-hostname--v2--hostname")
.paramLabel("hostname|URL"),
fromOption(HostnameV2Options.HOSTNAME_ADMIN)
.to("kc.spi-hostname-v2-hostname-admin")
.to("kc.spi-hostname--v2--hostname-admin")
.paramLabel("URL"),
fromOption(HostnameV2Options.HOSTNAME_BACKCHANNEL_DYNAMIC)
.to("kc.spi-hostname-v2-hostname-backchannel-dynamic"),
.to("kc.spi-hostname--v2--hostname-backchannel-dynamic"),
fromOption(HostnameV2Options.HOSTNAME_STRICT)
.to("kc.spi-hostname-v2-hostname-strict"),
.to("kc.spi-hostname--v2--hostname-strict"),
fromOption(HostnameV2Options.HOSTNAME_DEBUG)
)
.map(b -> b.isEnabled(() -> Profile.isFeatureEnabled(Profile.Feature.HOSTNAME_V2), "hostname:v2 feature is enabled").build())

View File

@ -31,7 +31,7 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.getOption
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
public final class ImportPropertyMappers {
private static final String IMPORTER_PROPERTY = "kc.spi-import-importer";
private static final String IMPORTER_PROPERTY = "kc.spi-import--importer";
private static final String SINGLE_FILE = "singleFile";
private static final String DIR = "dir";
@ -46,20 +46,20 @@ public final class ImportPropertyMappers {
.paramLabel("file")
.build(),
fromOption(ImportOptions.FILE)
.to("kc.spi-import-single-file-file")
.to("kc.spi-import--single-file--file")
.paramLabel("file")
.build(),
fromOption(ImportOptions.DIR)
.to("kc.spi-import-dir-dir")
.to("kc.spi-import--dir--dir")
.paramLabel("dir")
.build(),
fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-single-file-strategy")
.to("kc.spi-import--single-file--strategy")
.transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isSingleFileProvider)
.build(),
fromOption(ImportOptions.OVERRIDE)
.to("kc.spi-import-dir-strategy")
.to("kc.spi-import--dir--strategy")
.transformer(ImportPropertyMappers::transformOverride)
.isEnabled(ImportPropertyMappers::isDirProvider)
.build(),
@ -107,8 +107,8 @@ public final class ImportPropertyMappers {
return importer.getValue();
}
var file = getOptionalValue("kc.spi-import-single-file-file").map(f -> SINGLE_FILE);
var dir = getOptionalValue("kc.spi-import-dir-dir")
var file = getOptionalValue("kc.spi-import--single-file--file").map(f -> SINGLE_FILE);
var dir = getOptionalValue("kc.spi-import--dir--dir")
.or(() -> getOptionalValue("kc.dir"))
.map(f -> DIR);

View File

@ -18,12 +18,12 @@ final class MetricsPropertyMappers {
.to("quarkus.micrometer.enabled")
.build(),
fromOption(MetricsOptions.PASSWORD_VALIDATION_COUNTER_ENABLED)
.to("kc.spi-credential-keycloak-password-metrics-enabled")
.to("kc.spi-credential--keycloak-password--metrics-enabled")
.isEnabled(MetricsPropertyMappers::metricsEnabled, "metrics are enabled")
.build(),
fromOption(MetricsOptions.INFINISPAN_METRICS_ENABLED)
.mapFrom(MetricsOptions.METRICS_ENABLED)
.to("kc.spi-cache-embedded-default-metrics-enabled")
.to("kc.spi-cache-embedded--default--metrics-enabled")
.isEnabled(MetricsPropertyMappers::metricsEnabled, "metrics are enabled")
.build()
};

View File

@ -97,7 +97,11 @@ public final class PropertyMappers {
}
public static boolean isSpiBuildTimeProperty(String name) {
return name.startsWith(KC_SPI_PREFIX) && (name.endsWith("-provider") || name.endsWith("-enabled") || name.endsWith("-provider-default"));
return name.startsWith(KC_SPI_PREFIX) && (name.endsWith("--provider") || name.endsWith("--enabled") || name.endsWith("--provider-default"));
}
public static boolean isMaybeSpiBuildTimeProperty(String name) {
return name.startsWith(KC_SPI_PREFIX) && (name.endsWith("-provider") || name.endsWith("-enabled") || name.endsWith("-provider-default")) && !name.contains("--");
}
private static boolean isKeycloakRuntime(String name, PropertyMapper<?> mapper) {
@ -399,7 +403,15 @@ public final class PropertyMappers {
private static void handleMapper(PropertyMapper<?> mapper, BiConsumer<String, PropertyMapper<?>> operation) {
operation.accept(mapper.getFrom(), mapper);
if (!mapper.getFrom().equals(mapper.getTo())) {
operation.accept(mapper.getTo(), mapper);
String to = mapper.getTo();
operation.accept(to, mapper);
if (to.startsWith(KC_SPI_PREFIX)) {
if (!mapper.getTo().contains("--")) {
throw new IllegalStateException("Mapper should use the new form of the SPI option with the `--` separator: " + to);
}
String legacyTo = mapper.getTo().replace("--", "-");
operation.accept(legacyTo, mapper);
}
}
}
}

View File

@ -13,7 +13,7 @@ public class TruststorePropertyMappers {
.build(),
fromOption(TruststoreOptions.HOSTNAME_VERIFICATION_POLICY)
.paramLabel(TruststoreOptions.HOSTNAME_VERIFICATION_POLICY.getKey())
.to("kc.spi-truststore-file-hostname-verification-policy")
.to("kc.spi-truststore--file--hostname-verification-policy")
.build(),
};
}

View File

@ -15,19 +15,19 @@ final class VaultPropertyMappers {
.paramLabel("provider")
.build(),
fromOption(VaultOptions.VAULT_DIR)
.to("kc.spi-vault-file-dir")
.to("kc.spi-vault--file--dir")
.paramLabel("dir")
.build(),
fromOption(VaultOptions.VAULT_FILE)
.to("kc.spi-vault-keystore-file")
.to("kc.spi-vault--keystore--file")
.paramLabel("file")
.build(),
fromOption(VaultOptions.VAULT_PASS)
.to("kc.spi-vault-keystore-pass")
.to("kc.spi-vault--keystore--pass")
.paramLabel("pass")
.build(),
fromOption(VaultOptions.VAULT_TYPE)
.to("kc.spi-vault-keystore-type")
.to("kc.spi-vault--keystore--type")
.paramLabel("type")
.build()
};

View File

@ -71,9 +71,9 @@ quarkus.http.limits.max-header-size=65535
%dev.kc.http-enabled=true
%dev.kc.hostname-strict=false
%dev.kc.cache=local
%dev.kc.spi-theme-cache-themes=false
%dev.kc.spi-theme-cache-templates=false
%dev.kc.spi-theme-static-max-age=-1
%dev.kc.spi-theme--cache-themes=false
%dev.kc.spi-theme--cache-templates=false
%dev.kc.spi-theme--static-max-age=-1
# The default configuration when running in import or export mode
%nonserver.kc.http-enabled=true

View File

@ -610,6 +610,20 @@ public class PicocliTest extends AbstractConfigurationTest {
assertThat(nonRunningPicocli.getOutString(), containsString("Please use the first-class option `kc.hostname` instead of `kc.spi-hostname-v2-hostname`"));
}
@Test
public void testAmbiguousSpiOption() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--spi-x-y-enabled=true");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getOutString(), containsString("The following spi options are using the legacy format and are not being treated as build time options. Please use the new format with the appropriate -- separators to resolve this ambiguity: kc.spi-x-y-enabled"));
}
@Test
public void testAmbiguousSpiOptionBuild() {
NonRunningPicocli nonRunningPicocli = pseudoLaunch("build", "--db=dev-file", "--spi-x-y-enabled=true");
assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getOutString(), containsString("The following spi options are using the legacy format and are not being treated as build time options. Please use the new format with the appropriate -- separators to resolve this ambiguity: kc.spi-x-y-enabled"));
}
@Test
public void testDerivedShowConfig() {
NonRunningPicocli nonRunningPicocli = build("build", "--metrics-enabled=true", "--features=user-event-metrics", "--event-metrics-user-enabled=true", "--db=dev-file");
@ -748,4 +762,16 @@ public class PicocliTest extends AbstractConfigurationTest {
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: '--db-kind-<default>'"));
}
@Test
public void errorSpiBuildtimeChanged() {
putEnvVar("KC_SPI_EVENTS_LISTENER__PROVIDER", "jboss-logging");
build("build", "--db=dev-file");
putEnvVar("KC_SPI_EVENTS_LISTENER__PROVIDER", "new-jboss-logging");
NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--optimized", "--http-enabled=true", "--hostname-strict=false");
assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode);
assertThat(nonRunningPicocli.getErrString(), containsString("The following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run: kc.spi-events-listener--provider"));
}
}

View File

@ -65,6 +65,14 @@ public class ConfigurationTest extends AbstractConfigurationTest {
assertEquals(value, "foobar");
}
@Test
public void testCamelCaseNewFormat() {
putEnvVar("KC_SPI_CAMEL_CASE_SCOPE__CAMEL_CASE_PROP", "foobar");
initConfig();
String value = Config.scope("camelCaseScope").get("camelCaseProp");
assertEquals(value, "foobar");
}
@Test
public void testEnvVarPriorityOverPropertiesFile() {
putEnvVar("KC_SPI_HOSTNAME_DEFAULT_FRONTEND_URL", "http://envvar.unittest");
@ -84,13 +92,13 @@ public class ConfigurationTest extends AbstractConfigurationTest {
Config.Scope config = initConfig("vault", FilesPlainTextVaultProviderFactory.ID);
assertEquals("/foo/bar", config.get("dir"));
assertTrue(config.getPropertyNames()
.contains("kc.spi-vault-".concat(FilesPlainTextVaultProviderFactory.ID).concat("-dir")));
.contains("kc.spi-vault--".concat(FilesPlainTextVaultProviderFactory.ID).concat("--dir")));
putEnvVar("KC_VAULT_TYPE", "JKS");
config = initConfig("vault", FilesKeystoreVaultProviderFactory.ID);
assertEquals("JKS", config.get("type"));
assertTrue(config.getPropertyNames()
.contains("kc.spi-vault-".concat(FilesKeystoreVaultProviderFactory.ID).concat("-type")));
.contains("kc.spi-vault--".concat(FilesKeystoreVaultProviderFactory.ID).concat("--type")));
}
@Test
@ -183,7 +191,6 @@ public class ConfigurationTest extends AbstractConfigurationTest {
public void testPropertyNamesFromConfig() {
ConfigArgsConfigSource.setCliArgs("--spi-client-registration-openid-connect-static-jwk-url=http://c.jwk.url");
Config.Scope config = initConfig("client-registration", "openid-connect");
assertEquals(1, config.getPropertyNames().size());
assertEquals("http://c.jwk.url", config.get("static-jwk-url"));
ConfigArgsConfigSource.setCliArgs("--vault-dir=secrets");
@ -199,18 +206,15 @@ public class ConfigurationTest extends AbstractConfigurationTest {
ConfigArgsConfigSource.setCliArgs();
System.setProperty("kc.spi-client-registration-openid-connect-static-jwk-url", "http://c.jwk.url");
config = initConfig("client-registration", "openid-connect");
assertEquals(1, config.getPropertyNames().size());
assertEquals("http://c.jwk.url", config.get("static-jwk-url"));
ConfigArgsConfigSource.setCliArgs();
System.getProperties().remove("kc.spi-client-registration-openid-connect-static-jwk-url");
putEnvVar("KC_SPI_CLIENT_REGISTRATION_OPENID_CONNECT_STATIC_JWK_URL", "http://c.jwk.url/from-env");
config = initConfig("client-registration", "openid-connect");
assertEquals(2, config.getPropertyNames().size()); // transformed name is coming from KcEnvVarConfigSource, raw env var name is coming from EnvVarConfigSource
assertEquals("http://c.jwk.url/from-env", config.get("static-jwk-url"));
}
@Test
public void testPropertyMapping() {
ConfigArgsConfigSource.setCliArgs("--db=mariadb", "--db-url=jdbc:mariadb://localhost/keycloak");

View File

@ -0,0 +1,41 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime.configuration.mappers;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class PropertyMappersTest {
@Test
public void testIsSpiBuildTimeProperty() {
// Should return true for valid SPI build-time properties
assertTrue(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar--provider"));
assertTrue(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar--enabled"));
assertTrue(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar--provider-default"));
// return false for non-build time properties
assertFalse(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar-provider"));
assertFalse(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar-enabled"));
assertFalse(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar-provider-default"));
assertFalse(PropertyMappers.isSpiBuildTimeProperty("some.other.property"));
assertFalse(PropertyMappers.isSpiBuildTimeProperty("kc.spi.foo.bar"));
}
}

View File

@ -72,11 +72,13 @@ public class OptionsDistTest {
@Test
@Order(5)
@WithEnvVars({"KC_LOG", "console", "KC_LOG_FILE", "something-env", "KC_HTTP_ENABLED", "true", "KC_HOSTNAME_STRICT", "false"})
@WithEnvVars({"KC_SPI_CONNECTIONS_HTTP_CLIENT__DEFAULT__EXPECT_CONTINUE_ENABLED", "true", "KC_LOG", "console", "KC_LOG_FILE", "something-env", "KC_HTTP_ENABLED", "true", "KC_HOSTNAME_STRICT", "false"})
@Launch({"start", "--db=dev-file"})
public void testSettingEnvVars(CLIResult cliResult) {
cliResult.assertMessage("The following used run time options are UNAVAILABLE and will be ignored during build time:");
cliResult.assertMessage("- log-file: Available only when File log handler is activated.");
cliResult.assertNoMessage("kc.spi-connections-http-client"); // no info/warning expected
cliResult.assertStarted();
}
@DryRun

View File

@ -76,12 +76,12 @@ public class StartCommandDistTest {
CLIResult cliResult = dist.run("build", "--db=dev-file");
cliResult.assertBuild();
cliResult = dist.run("start", "--optimized", "--http-enabled=true", "--hostname-strict=false", "--spi-events-listener-jboss-logging-enabled=false");
cliResult.assertError("The following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run: kc.spi-events-listener-jboss-logging-enabled");
cliResult = dist.run("start", "--optimized", "--http-enabled=true", "--hostname-strict=false", "--spi-events-listener--jboss-logging--enabled=false");
cliResult.assertError("The following build time options have values that differ from what is persisted - the new values will NOT be used until another build is run: kc.spi-events-listener--jboss-logging--enabled");
}
@DryRun
@WithEnvVars({"KC_SPI_EVENTS_LISTENER_JBOSS_LOGGING_ENABLED", "false"})
@WithEnvVars({"KC_SPI_EVENTS_LISTENER__JBOSS_LOGGING__ENABLED", "false"})
@Test
@RawDistOnly(reason = "Containers are immutable")
void noErrorSpiBuildtimeNotChanged(KeycloakDistribution dist) {

View File

@ -69,14 +69,14 @@ public class SpiProvidersSwitchingUtils {
@Override
public void setDefaultProvider(Container container, String spiName, String providerId, String... config) {
List<String> args = new LinkedList<>();
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-provider=" + providerId);
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "--provider=" + providerId);
if (config != null) {
String optionName = null;
for (String c : config) {
if (optionName == null) {
optionName = c;
} else {
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "-" + providerId + "-" + optionName + "=" + c);
args.add(KEYCLOAKX_ARG_SPI_PREFIX + toDashCase(spiName) + "--" + providerId + "--" + optionName + "=" + c);
optionName = null;
}
}
@ -197,7 +197,7 @@ public class SpiProvidersSwitchingUtils {
Container container = authServerInfo.getArquillianContainer();
log.infof("Removing default provider setting for %s", spi);
if (annotation.onlyUpdateDefault()) {
spiSwitcher.unsetDefaultProvider(container, spi);
} else {