diff --git a/.github/scripts/version-compatibility.sh b/.github/scripts/version-compatibility.sh new file mode 100755 index 000000000000..fa42ccf1a9b9 --- /dev/null +++ b/.github/scripts/version-compatibility.sh @@ -0,0 +1,25 @@ +#!/bin/bash -e + +if [[ "$RUNNER_DEBUG" == "1" ]]; then + set -x +fi + +TARGET_BRANCH="$1" +REPO="${2:-keycloak}" +ORG="${3:-keycloak}" + +if [[ "${TARGET_BRANCH}" != "release/"* ]]; then + exit 0 +fi + +ALL_RELEASES=$(gh release list \ + --repo "${ORG}/${REPO}" \ + --exclude-drafts \ + --exclude-pre-releases \ + --json name \ + --template '{{range .}}{{.name}}{{"\n"}}{{end}}' +) +MAJOR_MINOR=${TARGET_BRANCH#"release/"} +MAJOR_MINOR_RELEASES=$(echo "${ALL_RELEASES}" | grep "${MAJOR_MINOR}") + +echo "${MAJOR_MINOR_RELEASES}" | jq -cnR '[inputs] | map({version: .})' \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e40e558ec5ba..78152ad3215c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: ci-sssd: ${{ steps.conditional.outputs.ci-sssd }} ci-webauthn: ${{ steps.conditional.outputs.ci-webauthn }} ci-aurora: ${{ steps.auroradb-tests.outputs.run-aurora-tests }} + ci-compatibility-matrix: ${{ steps.version-compatibility.outputs.matrix }} permissions: contents: read pull-requests: read @@ -57,6 +58,20 @@ jobs: fi echo "run-aurora-tests=$RUN_AURORADB_TESTS" >> $GITHUB_OUTPUT + - name: Version Compatibility Matrix + id: version-compatibility + env: + GH_TOKEN: ${{ github.token }} + run: | + if [[ "${{ github.event_name }}" == "pull_request" ]]; then + BRANCH="${{ github.base_ref }}" + else + BRANCH="${{ github.ref_name }}" + fi + MATRIX_JSON=$(./.github/scripts/version-compatibility.sh "${BRANCH}") + echo "${MATRIX_JSON}" + echo "matrix=${MATRIX_JSON}" >> $GITHUB_OUTPUT + build: name: Build if: needs.conditional.outputs.ci == 'true' @@ -1105,6 +1120,36 @@ jobs: - name: Run tests run: ./mvnw package -f tests/pom.xml + mixed-cluster-compatibility-tests: + name: Cluster Compatibility Tests + if: needs.conditional.outputs.ci-compatibility-matrix != '' + runs-on: ubuntu-latest + needs: + - build + - conditional + strategy: + fail-fast: false + matrix: + include: ${{ fromJSON(needs.conditional.outputs.ci-compatibility-matrix) }} + timeout-minutes: 10 + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - id: integration-test-setup + name: Integration test setup + uses: ./.github/actions/integration-test-setup + + # This step is necessary because test/clustering requires building a new Keycloak image built from tar.gz + # file that is not part of m2-keycloak.tzts archive + - name: Build tar keycloak-quarkus-dist + run: ./mvnw package -pl quarkus/server/,quarkus/dist/ + + - name: Run tests + run: ./mvnw verify -pl tests/clustering + env: + KC_TEST_SERVER_IMAGES: "quay.io/keycloak/keycloak:${{ matrix.version }},-" + check: name: Status Check - Keycloak CI if: always() @@ -1131,6 +1176,7 @@ jobs: - external-infinispan-tests - test-framework - base-new-integration-tests + - mixed-cluster-compatibility-tests runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/ADOPTERS.md b/ADOPTERS.md index 543bfefa75ab..00f9e50dd23e 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -63,4 +63,5 @@ List of organization names below is based on information collected using Keycloa * TRT9 - Brasil * UnitedHealthcare * Wayfair LLC +* [Xata](https://xata.io) * ...More individuals diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 173dade71e87..a9aa88dc22ed 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -3,7 +3,6 @@ * [Alexander Schwartz](https://github.com/ahus1) * [Bruno Oliveira da Silva](https://github.com/abstractj) * [Marek Posolda](https://github.com/mposolda) -* [Michal Hajas](https://github.com/mhajas) * [Pedro Igor](https://github.com/pedroigor) * [Sebastian Schuster](https://github.com/sschu) * [Stan Silvert](https://github.com/ssilvert) @@ -15,5 +14,6 @@ # Emeritus maintainers * [Hynek Mlnařík](https://github.com/hmlnarik) +* [Michal Hajas](https://github.com/mhajas) * [Pavel Drozd](https://github.com/pdrozd) diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index 2c9edbe70850..84d0c47f9698 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -124,7 +124,7 @@ public enum Feature { ORGANIZATION("Organization support within realms", Type.DEFAULT), - PASSKEYS("Passkeys", Type.PREVIEW), + PASSKEYS("Passkeys", Type.PREVIEW, Feature.WEB_AUTHN), USER_EVENT_METRICS("Collect metrics based on user events", Type.DEFAULT), diff --git a/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java index a4b6400a1641..31eb0eb60b93 100644 --- a/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/ConsentScopeRepresentation.java @@ -23,15 +23,15 @@ public class ConsentScopeRepresentation { private String name; - private String displayTest; + private String displayText; public ConsentScopeRepresentation() { } - public ConsentScopeRepresentation(String id, String name, String displayTest) { + public ConsentScopeRepresentation(String id, String name, String displayText) { this.id = id; this.name = name; - this.displayTest = displayTest; + this.displayText = displayText; } public String getId() { @@ -50,11 +50,27 @@ public void setName(String name) { this.name = name; } + public String getDisplayText() { + return displayText; + } + + public void setDisplayText(String displayText) { + this.displayText = displayText; + } + + /** + * @deprecated Use {@link #getDisplayText()} instead. This method will be removed in KC 27.0. + */ + @Deprecated public String getDisplayTest() { - return displayTest; + return displayText; } + /** + * @deprecated Use {@link #setDisplayText(String)} instead. This method will be removed in KC 27.0. + */ + @Deprecated public void setDisplayTest(String displayTest) { - this.displayTest = displayTest; + this.displayText = displayTest; } } diff --git a/docs/documentation/server_admin/topics/sessions/offline.adoc b/docs/documentation/server_admin/topics/sessions/offline.adoc index 67732408db24..ab47f7e24bb4 100644 --- a/docs/documentation/server_admin/topics/sessions/offline.adoc +++ b/docs/documentation/server_admin/topics/sessions/offline.adoc @@ -24,20 +24,4 @@ Clients can request an offline token by adding the parameter `scope=offline_acce {project_name} will limit its internal cache for offline user and offline client sessions to 10000 entries by default, which will reduce the overall memory usage for offline sessions. Items which are evicted from memory will be loaded on-demand from the database when needed. -To set different sizes for the caches, edit {project_name}'s cache config file to set a `++` for those caches. - -If you disabled feature `persistent-user-sessions`, it is possible to reduce memory requirements using a configuration option that shortens lifespan for imported offline sessions. Such sessions will be evicted from the Infinispan caches after the specified lifespan, but still available in the database. This will lower memory consumption, especially for deployments with a large number of offline sessions. - -To specify the lifespan override for offline user sessions, start {project_name} server with the following parameter: - -[source,bash] ----- ---spi-user-sessions--infinispan--offline-session-cache-entry-lifespan-override= ----- - -Similarly for offline client sessions: - -[source,bash] ----- ---spi-user-sessions--infinispan--offline-client-session-cache-entry-lifespan-override= ----- +See the server configuration guide to change this default. diff --git a/docs/documentation/server_development/topics/themes.adoc b/docs/documentation/server_development/topics/themes.adoc index fc370ac57ee0..d52feea014bc 100644 --- a/docs/documentation/server_development/topics/themes.adoc +++ b/docs/documentation/server_development/topics/themes.adoc @@ -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. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc index fad279451c9e..f0bbc83e478f 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_2_0.adoc @@ -181,3 +181,8 @@ We don't expect that this will impact any deployments due to the lack of support === JWT client authentication defines a new max expiration option for the token When a client is configured to authenticate using the *Signed JWT* or *Signed JWT with Client Secret* type, {project_name} now enforces a maximum expiration for the token. This means that, although the `exp` (expiration) claim in the token may be much later, {project_name} will not accept tokens issued before that max expiration time. The default value is 60 seconds. Note that JWT tokens should be issued right before being sent for authentication. This way, the client has one minute window to send the token for login. Nevertheless this expiration can be tuned using the *Max expiration* configuration option in the client *Credentials* tab (see link:{adminguide_link}#_client-credentials[Confidential client credentials in the {adminguide_name}] for more information). + +=== Updates to the `user-profile-commons.ftl` theme template +The `user-profile-commons.ftl` changed to improve support for localization. See https://github.com/keycloak/keycloak/issues/38029. +As a result, and if you are extending this template, pages might start displaying a `locale` field. To avoid that, update +the theme template with the changes aforementioned. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_3_1.adoc b/docs/documentation/upgrading/topics/changes/changes-26_3_1.adoc new file mode 100644 index 000000000000..884556ac5cd6 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_3_1.adoc @@ -0,0 +1,8 @@ +// ------------------------ Notable changes ------------------------ // +== Notable changes + +Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. + +=== Options for additional datasources marked as preview + +In the 26.3.0 release, the newly added options for configuring additional datasources were missing a preview label. This has been now corrected as the work on this feature continues over a few next releases. diff --git a/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc new file mode 100644 index 000000000000..54daaf12cb09 --- /dev/null +++ b/docs/documentation/upgrading/topics/changes/changes-26_4_0.adoc @@ -0,0 +1,74 @@ +// ------------------------ Breaking changes ------------------------ // +== Breaking changes + +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. + +=== + +// ------------------------ Notable changes ------------------------ // +== Notable changes + +Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. + +=== Usage of the `exact` request parameter when searching users by attributes + +If you are querying users by attributes through the User API where you want to fetch users that match a specific attribute key (regardless the value), +you should consider setting the `exact` request parameter to `false` when invoking the `+/admin/realms/{realm}/users+` using +the `GET` method. + +For instance, searching for all users with the attribute `myattribute` set should be done as follows: + +[source] +---- +GET /admin/realms/{realm}/users?exact=false&q=myattribute: +---- + +The {project_name} Admin Client is also updated with a new method to search users by attribute using the `exact` request parameter. + +=== Automatic database connection properties for the PostgreSQL driver + +When running PostgreSQL reader and writer instances, {project_name} needs to always connect to the writer instance to do its work. + +Starting with this release, and when using the original PostgreSQL driver, {project_name} sets the `targetServerType` property of the PostgreSQL JDBC driver to `primary` to ensure that it always connects to a writable primary instance and never connects to a secondary reader instance in failover or switchover scenarios. + +You can override this behavior by setting your own value for `targetServerType` in the DB URL or additional properties. + +=== JGroups system properties replaced with CLI options + +Until now it was necessary to configure JGroups network addresses and ports using the `+jgroups.bind.*+` and `+jgroups.external_*+` +system properties. In this release we have introduced the following CLI options to allow these addresses and ports to be +configured directly via {project_name}: `cache-embedded-network-bind-address`, `cache-embedded-network-bind-port`, +`cache-embedded-network-external-address`, `cache-embedded-network-external-port`. Configuring ports using the old +properties will still function as before, but we recommend to change to the CLI options as this may change in the future. + +=== Volatile user sessions affecting offline session memory requirements + +Starting with this release, {project_name} will cache by default only 10000 entries for offline user and client sessions in memory when volatile user sessions are enabled. This will greatly reduce memory usage. + +Use the options `cache-embedded-offline-sessions-max-count` and `cache-embedded-offline-client-sessions-max-count` to change size of the offline session caches. + +// ------------------------ Deprecated features ------------------------ // +== Deprecated features + +The following sections provide details on deprecated features. + +=== Deprecated `displayTest` field in `ConsentScopeRepresentation` + +The `displayTest` field in the `ConsentScopeRepresentation` class returned by the Account REST service has been deprecated due to a typo in its name. +A new field `displayText` with the correct spelling has been added to replace it. The old field will be removed in {project_name} 27.0. +The Typescript code `ConsentScopeRepresentation` for the Account Console already contains only the new field. + +=== Lifetime of offline session caches + +The options `--spi-user-sessions--infinispan--offline-session-cache-entry-lifespan-override` and `spi-user-sessions--infinispan--offline-client-session-cache-entry-lifespan-override` are now deprecated for removal. + +Instead use the options `cache-embedded-offline-sessions-max-count` and `cache-embedded-offline-client-sessions-max-count` to limit the memory usage if the default of 10000 cache offline user and client sessions does not work in your scenario. + +// ------------------------ Removed features ------------------------ // +== Removed features + +The following features have been removed from this release. + +=== + diff --git a/docs/documentation/upgrading/topics/changes/changes.adoc b/docs/documentation/upgrading/topics/changes/changes.adoc index 198c6ba52110..ea7f6ba5d3c3 100644 --- a/docs/documentation/upgrading/topics/changes/changes.adoc +++ b/docs/documentation/upgrading/topics/changes/changes.adoc @@ -1,6 +1,10 @@ [[migration-changes]] == Migration Changes +=== Migrating to 26.4.0 + +include::changes-26_4_0.adoc[leveloffset=2] + === Migrating to 26.3.0 include::changes-26_3_0.adoc[leveloffset=2] diff --git a/docs/guides/server/caching.adoc b/docs/guides/server/caching.adoc index 10028c091d41..be308a8eeb0e 100644 --- a/docs/guides/server/caching.adoc +++ b/docs/guides/server/caching.adoc @@ -140,12 +140,6 @@ Since all the sessions in this setup are stored in-memory, there are two side ef When using volatile user sessions, the cache is the source of truth for user and client sessions. {project_name} automatically adjusts the number of entries that can be stored in memory, and increases the number of copies to prevent data loss. -[WARNING] -==== -It is not recommended to use volatile user sessions when using offline sessions extensively due to potentially high memory usage. -For volatile sessions, the time offline sessions are cached in memory can be limited with the SPI options `spi-user-sessions--infinispan--offline-client-session-cache-entry-lifespan-override` and `spi-user-sessions--infinispan--offline-session-cache-entry-lifespan-override`. -==== - Follow these steps to enable this setup: 1. Disable `persistent-user-sessions` feature using the following command: @@ -321,22 +315,25 @@ To ensure a healthy {project_name} clustering, some network ports need to be ope The table below shows the TCP ports that need to be open for the `jdbc-ping` stack, and a description of the traffic that goes through it. |=== -|Port |Property | Description +|Port |Option| Property | Description m|7800 +m|cache-embedded-network-bind-port m|jgroups.bind.port |Unicast data transmission. m|57800 +m| m|jgroups.fd.port-offset |Failure detection by protocol `FD_SOCK2`. It listens to the abrupt closing of a socket to suspect a {project_name} server failure. -The `jgroups.fd.port-offset` property defines the offset from the `jgroups.bind.port`. +The `jgroups.fd.port-offset` property defines the offset from the `cache-embedded-network-bind-port` option or `jgroups.bind.port` property. By default, the offset is set to 50000, making the failure detection port 57800. |=== -NOTE: Use `-D=` to modify the ports above in your `JAVA_OPTS_APPEND` environment variable or in your CLI command. +NOTE: If an option is not available for the port you require, configure it using a system property `-D=` +in your `JAVA_OPTS_APPEND` environment variable or in your CLI command. [#network-bind-address] == Network bind address @@ -345,11 +342,9 @@ To ensure a healthy {project_name} clustering, the network port must be bound on By default, it picks a site local (non-routable) IP address, for example, from the 192.168.0.0/16 or 10.0.0.0/8 address range. -To override the address, set the `jgroups.bind.address` property. +To override the address, set the option `cache-embedded-network-bind-address=`. -NOTE: Use `-Djgroups.bind.address=` to modify the bind address in your `JAVA_OPTS_APPEND` environment variable or in your CLI command. - -The following special values are also recognized for `jgroups.bind.address`: +The following special values are also recognized: |=== |Value |Description @@ -399,21 +394,19 @@ For more details about JGroups transport, check the http://jgroups.org/manual5/i If you run {project_name} instances on different networks, for example behind firewalls or in containers, the different instances will not be able to reach each other by their local IP address. In such a case, set up a port forwarding rule (sometimes called "`virtual server`") to their local IP address. -When using port forwarding, use the following properties so each node correctly advertises its external address to the other nodes: +When using port forwarding, use the following options so each node correctly advertises its external address to the other nodes: |=== -|Property | Description +|Option | Description -m|jgroups.external_port +m|cache-embedded-network-external-port |Port that other instances in the {project_name} cluster should use to contact this node. -m|jgroups.external_addr +m|cache-embedded-network-external-address |IP address that other instances in the {project_name} should use to contact this node. |=== -NOTE: Use `-D=` set this in your `JAVA_OPTS_APPEND` environment variable or in your CLI command. - == Exposing metrics from caches Metrics from caches are automatically exposed when the metrics are enabled. diff --git a/docs/guides/server/configuration.adoc b/docs/guides/server/configuration.adoc index 0fcc85caebc5..150d16134336 100644 --- a/docs/guides/server/configuration.adoc +++ b/docs/guides/server/configuration.adoc @@ -167,10 +167,13 @@ PowerShell handles quotes differently. It interprets the quoted string before pa === Formats for environment variable keys with special characters -Some configuration keys, such as those for logging, may contain characters such as `_` or `$` - e.g. `kc.log-level-package.class_name`. Non-alphanumeric characters in your configuration key must be converted to `\_` in the corresponding environment variable key. +Non-alphanumeric characters in your configuration key must be converted to `_` in the corresponding environment variable key. -The automatic handling of the environment variable key may not preserve the intended key. For example `KC_LOG_LEVEL_PACKAGE_CLASS_NAME` will become `kc.log-level-package.class.name` because logging properties default to replacing `_` in the "wildcard" -part of the key with `.` as that is what is most commonly needed in a class / package name. However this does not match the intent of `kc.log-level-package.class_name`. +Environment variables are converted back to normal option keys by lower-casing the name and replacing `\_` with `-`. Logging wildcards are the exception as `_` in the category is replaced with `.` instead. Logging categories are commonly class / package names, which are more likely to use `.` rather than `-`. + +WARNING: Automatic mapping of the environment variable key to option key may not preserve the intended key + +For example `kc.log-level-package.class_name` will become the environment variable key `KC_LOG_LEVEL_PACKAGE_CLASS_NAME`. That will automatically be mapped to `kc.log-level-package.class.name` because `_` in the logging category will be replaced by `.`. Unfortunately this does not match the intent of `kc.log-level-package.class_name`. You have a couple of options in this case: diff --git a/docs/guides/server/db.adoc b/docs/guides/server/db.adoc index 85a93d0744b5..98cd28d85a53 100644 --- a/docs/guides/server/db.adoc +++ b/docs/guides/server/db.adoc @@ -271,6 +271,18 @@ show server_encoding; create database keycloak with encoding 'UTF8'; ---- +== Preparing for PostgreSQL + +When running PostgreSQL reader and writer instances, {project_name} needs to always connect to the writer instance to do its work. +When using the original PostgreSQL driver, {project_name} sets the `targetServerType` property of the PostgreSQL JDBC driver to `primary` to ensure that it always connects to a writable primary instance and never connects to a secondary reader instance in failover or switchover scenarios. + +You can override this behavior by setting your own value for `targetServerType` in the DB URL or additional properties. + +[NOTE] +==== +The `targetServerType` is only applied automatically to the primary datasource, as requirements might be different for additional datasources. +==== + [[preparing-keycloak-for-amazon-aurora-postgresql]] == Preparing for Amazon Aurora PostgreSQL @@ -296,6 +308,8 @@ See the <@links.server id="containers" /> {section} for details on how to build `db-url`:: Insert `aws-wrapper` to the regular PostgreSQL JDBC URL resulting in a URL like `+jdbc:aws-wrapper:postgresql://...+`. `db-driver`:: Set to `software.amazon.jdbc.Driver` to use the AWS JDBC wrapper. +NOTE: When overriding the `wrapperPlugins` option of the AWS JDBC Driver, always include the `failover` or `failover2` plugin to ensure that {project_name} always connects to the writer instance even in failover or switchover scenarios. + == Preparing for MySQL server Beginning with MySQL 8.0.30, MySQL supports generated invisible primary keys for any InnoDB table that is created without an explicit primary key (more information https://dev.mysql.com/doc/refman/8.0/en/create-table-gipks.html[here]). diff --git a/docs/guides/server/update-compatibility.adoc b/docs/guides/server/update-compatibility.adoc index 0158a9a13f79..2913be824455 100644 --- a/docs/guides/server/update-compatibility.adoc +++ b/docs/guides/server/update-compatibility.adoc @@ -170,6 +170,8 @@ Known limitations: * If there have been changes to the Account Console or Admin UI in the patch release, and the user opened the Account Console or Admin UI before or during the upgrade, the user might see an error message and be asked to reload the application while navigating in browser during or after the upgrade. +* If the two patch releases of {project_name} use different versions of the embedded Infinispan, no rolling update of {project_name} be performed. + == Further reading The {project_name} Operator uses the functionality described above to determine if a rolling update is possible. See the <@links.operator id="rolling-updates" /> {section} and the `Auto` strategy for more information. diff --git a/docs/guides/ui-customization/themes.adoc b/docs/guides/ui-customization/themes.adoc index 7d53826a3aa0..a7cf860db926 100644 --- a/docs/guides/ui-customization/themes.adoc +++ b/docs/guides/ui-customization/themes.adoc @@ -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. diff --git a/docs/translation.md b/docs/translation.md index 61af523b4ffb..54dce037edd4 100644 --- a/docs/translation.md +++ b/docs/translation.md @@ -81,7 +81,7 @@ It allows for notifications when the original string changes, and keeps track of The following translations are available in Weblate. If you have any questions or need assistance, feel free to reach out to the language maintainers listed below: * German: [Robin Meese](https://github.com/robson90) && [Alexander Schwartz](https://github.com/ahus1) -* Dutch: [Jon Koops](https://github.com/jonkoops) && [janher](https://github.com/janher) +* Dutch: [janher](https://github.com/janher) * Japanese: [y-tabata](https://github.com/y-tabata) && [wadahiro](https://github.com/wadahiro) && [tnorimat](https://github.com/tnorimat) && [k-tamura](https://github.com/k-tamura) * Catalan: [jmallach](https://github.com/jmallach) && [Ecron](https://github.com/Ecron) * Spanish: [herver1971](https://github.com/herver1971) && [anthieni](https://github.com/anthieni) diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java index 7498a3ee9a1c..32b1f3d74a3d 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/Keycloak.java @@ -101,39 +101,80 @@ private static Client newRestEasyClient(Object customJacksonProvider, SSLContext private BearerAuthFilter newAuthFilter() { return authToken != null ? new BearerAuthFilter(authToken) : new BearerAuthFilter(tokenManager); } - + + /** + * + * Creates the java admin client instance to be used to call admin REST API against Keycloak server. + * + * @param serverUrl Keycloak server URL + * @param realm realm name + * @param username username of the admin user to be used. + * @param password password of the admin user + * @param clientId client ID + * @param clientSecret client secret. Could be left null in case that clientId parameter points to the public client, which does not require client authentication + * @param sslContext ssl context. Could be left null in case that default SSL context should be used. + * @param customJacksonProvider custom Jackson provider. Could be left null in case that Jackson provider will be automatically provided by the admin client. Please see the documentation for additional details regarding the compatibility + * @param disableTrustManager If to disable trust manager for SSL checks. It is false by default. The value true should be used just for the development purposes, but should not be used in production + * @param authToken access token to be used to call admin REST API. This can be left null if you want admin client to login the user (based on the parameters username, password, clientId and clientSecret) and manage it's own login session. But in case you already have existing session, you can inject the existing access token with the use of this parameter. In that case, it is recommended to leave the properties username, password, clientId or clientSecret empty + * @param scope Custom "scope" parameter to be used. Could be left null in case of default scope should be used. That is sufficient for most of the cases. + * @return Java admin client instance + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken, String scope) { return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, scope); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider, boolean disableTrustManager, String authToken) { return new Keycloak(serverUrl, realm, username, password, clientId, clientSecret, PASSWORD, newRestEasyClient(customJacksonProvider, sslContext, disableTrustManager), authToken, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret) { return getInstance(serverUrl, realm, username, password, clientId, clientSecret, null, null, false, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext) { return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, null, false, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, String clientSecret, SSLContext sslContext, Object customJacksonProvider) { return getInstance(serverUrl, realm, username, password, clientId, clientSecret, sslContext, customJacksonProvider, false, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId) { return getInstance(serverUrl, realm, username, password, clientId, null, null, null, false, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String username, String password, String clientId, SSLContext sslContext) { return getInstance(serverUrl, realm, username, password, clientId, null, sslContext, null, false, null); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken) { return getInstance(serverUrl, realm, null, null, clientId, null, null, null, false, authToken); } + /** + * See {@link #getInstance(String, String, String, String, String, String, SSLContext, Object, boolean, String, String)} for the details about the parameters and their default values + */ public static Keycloak getInstance(String serverUrl, String realm, String clientId, String authToken, SSLContext sllSslContext) { return getInstance(serverUrl, realm, null, null, clientId, null, sllSslContext, null, false, authToken); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java index dd0fb0579fee..cfb9f0034049 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/KeycloakBuilder.java @@ -35,7 +35,10 @@ * .password("pass") * .clientId("client") * .clientSecret("secret") - * .resteasyClient(new ResteasyClientBuilder().connectionPoolSize(20).build()) + * .resteasyClient(new ResteasyClientBuilderImpl() + * .connectionPoolSize(20) + * .build() + * .register(org.keycloak.admin.client.JacksonProvider.class, 100)) * .build(); * *

Example usage with grant_type=client_credentials

@@ -105,6 +108,12 @@ public KeycloakBuilder clientSecret(String clientSecret) { return this; } + /** + * Custom instance of resteasy client. Please see the documentation for additional details regarding the compatibility + * + * @param resteasyClient Custom RestEasy client + * @return admin client builder + */ public KeycloakBuilder resteasyClient(Client resteasyClient) { this.resteasyClient = resteasyClient; return this; diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java index cf7d6c8ef4e8..75c7dc5027fe 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UsersResource.java @@ -129,6 +129,16 @@ List searchByAttributes(@QueryParam("first") Integer firstRe @QueryParam("briefRepresentation") Boolean briefRepresentation, @QueryParam("q") String searchQuery); + @GET + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + List searchByAttributes(@QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults, + @QueryParam("enabled") Boolean enabled, + @QueryParam("exact") Boolean exact, + @QueryParam("briefRepresentation") Boolean briefRepresentation, + @QueryParam("q") String searchQuery); + @GET @Produces(MediaType.APPLICATION_JSON) List search(@QueryParam("username") String username, @QueryParam("exact") Boolean exact); @@ -240,6 +250,29 @@ List search(@QueryParam("search") String search, @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults); + /** + * Returns the users that can be viewed and match the given filters. + * + * @param search arbitrary search string for all the fields below + * @param last last name field of a user + * @param first first name field of a user + * @param email email field of a user + * @param emailVerified emailVerified field of a user + * @param username username field of a user + * @param enabled Boolean representing if user is enabled or not + * @return the list of users matching the given filters + */ + @GET + @Produces(MediaType.APPLICATION_JSON) + List search(@QueryParam("search") String search, + @QueryParam("lastName") String last, + @QueryParam("firstName") String first, + @QueryParam("email") String email, + @QueryParam("emailVerified") Boolean emailVerified, + @QueryParam("username") String username, + @QueryParam("enabled") Boolean enabled, + @QueryParam("q") String searchQuery); + @GET @Produces(MediaType.APPLICATION_JSON) List list(@QueryParam("first") Integer firstResult, @@ -338,6 +371,33 @@ Integer count(@QueryParam("search") String search, @QueryParam("enabled") Boolean enabled, @QueryParam("q") String searchQuery); + /** + * Returns the number of users that can be viewed and match the given filters. + * If none of the filters is specified this is equivalent to {{@link #count()}}. + * + * @param search arbitrary search string for all the fields below + * @param last last name field of a user + * @param first first name field of a user + * @param email email field of a user + * @param emailVerified emailVerified field of a user + * @param username username field of a user + * @param enabled Boolean representing if user is enabled or not + * @return number of users matching the given filters + */ + @Path("count") + @GET + @Produces(MediaType.APPLICATION_JSON) + Integer count(@QueryParam("search") String search, + @QueryParam("lastName") String last, + @QueryParam("firstName") String first, + @QueryParam("email") String email, + @QueryParam("emailVerified") Boolean emailVerified, + @QueryParam("username") String username, + @QueryParam("enabled") Boolean enabled, + @QueryParam("idpAlias") String idpAlias, + @QueryParam("idpUserId") String idpUserId, + @QueryParam("q") String searchQuery); + /** * Returns the number of users with the given status for emailVerified. * If none of the filters is specified this is equivalent to {{@link #count()}}. diff --git a/js/apps/account-ui/maven-resources-community/theme/keycloak.v3/account/messages/messages_ro.properties b/js/apps/account-ui/maven-resources-community/theme/keycloak.v3/account/messages/messages_ro.properties index dbf3ac792d67..b3303e075cfe 100644 --- a/js/apps/account-ui/maven-resources-community/theme/keycloak.v3/account/messages/messages_ro.properties +++ b/js/apps/account-ui/maven-resources-community/theme/keycloak.v3/account/messages/messages_ro.properties @@ -1,5 +1,6 @@ -cancel=Renunțǎ +cancel=Renunță backTo=Înapoi la {{app}} required=Obligatoriu path=Cale password=Parola mea +infoMessage=Dacă apeși pe Elimină accesul, vei revoca permisiunile acordate acestei aplicații. Această aplicație nu va mai folosi informațiile tale. diff --git a/js/apps/account-ui/package.json b/js/apps/account-ui/package.json index a9a6474a870e..47ec85966bcd 100644 --- a/js/apps/account-ui/package.json +++ b/js/apps/account-ui/package.json @@ -30,27 +30,27 @@ "@patternfly/react-core": "^5.4.14", "@patternfly/react-icons": "^5.4.2", "@patternfly/react-table": "^5.4.16", - "i18next": "^25.2.1", + "i18next": "^25.3.1", "i18next-fetch-backend": "^6.0.0", "keycloak-js": "^26.2.0", "lodash-es": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.59.0", + "react-hook-form": "^7.60.0", "react-i18next": "^15.5.3", "react-router-dom": "^6.30.1" }, "devDependencies": { "@keycloak/keycloak-admin-client": "workspace:*", - "@playwright/test": "^1.53.1", + "@playwright/test": "^1.53.2", "@types/lodash-es": "^4.17.12", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", "@vitejs/plugin-react-swc": "^3.10.2", "cross-env": "^7.0.3", "lightningcss": "^1.30.1", - "vite": "^7.0.0", - "vite-plugin-checker": "^0.9.3", + "vite": "^7.0.3", + "vite-plugin-checker": "^0.10.0", "vite-plugin-dts": "^4.5.4" }, "wireit": { diff --git a/js/apps/account-ui/src/api/representations.ts b/js/apps/account-ui/src/api/representations.ts index 0c4bb10d31bc..944c0dd49ded 100644 --- a/js/apps/account-ui/src/api/representations.ts +++ b/js/apps/account-ui/src/api/representations.ts @@ -31,7 +31,7 @@ export interface ConsentRepresentation { export interface ConsentScopeRepresentation { id: string; name: string; - displayTest: string; + displayText: string; } export interface CredentialMetadataRepresentationMessage { diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties index ce2a731cfe46..8d4da9f76e39 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_es.properties @@ -3494,3 +3494,4 @@ webAuthnPolicyPasskeysEnabledHelp=Habilita la autenticación con llaves de acces deprecated=Obsoleto validity=Vencimiento del certificado validityHelp=Cantidad de años durante los cuales el certificado generado será válido. +tokenIntrospectionUrl=URL de introspección de token diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties index 5385cc10a0bf..2cfe76590df6 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_fr.properties @@ -3494,3 +3494,4 @@ webAuthnPolicyPasskeysEnabledHelp=Activer l'authentification avec des clés d'ac deprecated=Obsolète validity=Expiration de certificat validityHelp=Nombre d'années de validité du certificat généré. +tokenIntrospectionUrl=URL d'introspection du jeton diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties index 9cfdaf99f0d4..26658cc76f94 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ja.properties @@ -4,7 +4,7 @@ addClientScope=クライアントスコープの追加 clientType=クライアントタイプ clientSignature=クライアント署名が必須 unanimous=Unanimous -policy-name=このポリシーの名前を設定します。 +policy-name=このポリシーの名前。 clientHelp=認可リクエストを作成するクライアントを選択してください。提供されない場合は、認可リクエストは今いるページのクライアントで行われることになります。 disabledFeatures=無効な機能 wantAssertionsSignedHelp=このサービスプロバイダーが署名付きアサーションを要求するかどうかを設定します。 @@ -61,15 +61,15 @@ passwordPolicy=パスワードポリシー openIDEndpointConfiguration=OpenIDエンドポイントの設定 backchannelLogout=バックチャネルログアウト addressClaim.street.label=その他住所のユーザー属性名 -prompts.login=login +prompts.login=ログイン users=ユーザー offlineSessionIdleHelp=セッションの有効期限が切れるまでのオフライン時間です。この期限内に少なくとも1回はオフライントークンを使用してリフレッシュしないと、オフラインセッションは有効期限切れとなります。 wantAssertionsEncrypted=アサーションの暗号化が必要 -forceNameIdFormatHelp=要求されたNameIDサブジェクトフォーマットを無視し、管理コンソールで設定された物を使用します。 +forceNameIdFormatHelp=要求されたNameIDサブジェクトフォーマットを無視し、管理コンソールで設定されたものを使用します。 uris=URI port=ポート realmRolePrefix=レルムロールのプレフィックス -jwksUrlHelp=JWK形式のクライアント鍵が保存されているURLを設定します。詳細はJWKの仕様を参照してください。「jwt」クレデンシャルを持つKeycloakクライアントアダプターを使用している場合は、アプリケーションに「/k_jwks」という接尾辞を付けたURLを使用することができます。例えば、「http://www.myhost.com/myapp/k_jwks」です。 +jwksUrlHelp=JWK形式のアイデンティティープロバイダーキーが保存されるURL。詳細はJWK仕様を参照してください。外部Keycloakアイデンティティープロバイダーを使用する場合は、ブローカーであるKeycloakが「http://broker-keycloak:8180」で実行されており、レルムが「test」であると仮定すると、「http://broker-keycloak:8180/realms/test/protocol/openid-connect/certs」のようなURLを使用できます。 includeRepresentation=Representationを含める singleLogoutServiceUrl=シングルログアウトサービスのURL roles=ロール @@ -80,22 +80,22 @@ loginTheme=ログインテーマ provider=プロバイダー flows=フロー scope=スコープ -includeRepresentationHelp=作成または更新リクエストのJSON Representationを含めるかどうかを設定します。 +includeRepresentationHelp=作成または更新リクエストのJSON表現を含めるかどうかを設定します。 signAssertionsHelp=SAMLドキュメント内のアサーションを署名すべきか設定します。もしドキュメントが既に署名済みの場合は、この設定は不要です。 validateSignature=署名検証 headers=ヘッダー effectiveProtocolMappersHelp=すべてのデフォルトのクライアントスコープと選択されたオプションのスコープが含まれます。クライアントに発行されたアクセストークンを生成するときに、すべてのクライアントスコープのすべてのプロトコルマッパーとロールスコープのマッピングが使用されます。 -fromDisplayNameHelp=差出人のアドレスのユーザーフレンドリーな名前です(オプション)。 +fromDisplayNameHelp=差出人のアドレスのユーザーフレンドリーな名前(オプション)。 userObjectClasses=ユーザーオブジェクトクラス policyRoles=このポリシーで許可されるクライアントロールを指定してください。 accountLinkingOnlyHelp=オンの場合、ユーザーはこのプロバイダーからログインできません。このプロバイダーにリンクすることのみできます。これは、プロバイダーからのログインを許可したくないが、プロバイダーと統合したい場合に便利です -refreshTokenMaxReuseHelp=リフレッシュトークンを再利用できる最大回数。別のトークンが使用された場合、即時に無効化されます。 +refreshTokenMaxReuseHelp=リフレッシュトークンを再利用できる回数。別のトークンが使用された場合、即時に無効化されます。 times.hours=時 webOrigins=Webオリジン webAuthnPolicyAuthenticatorAttachmentHelp=受け入れ可能なアタッチメントパターンでオーセンティケーターと通信します。 username=ユーザー名 importConfig=ファイルから設定をインポート -replyToDisplayNameHelp=返信先のアドレスのユーザーフレンドリーな名前です(オプション)。 +replyToDisplayNameHelp=返信先のアドレスのユーザーフレンドリーな名前(オプション)。 lifespan=有効期限 storedTokensReadableHelp=新しいユーザーが保存されたトークンの読み取りを可能にするか切り替えます。broker.read-tokenロールをアサインします。 webAuthnPolicyRpIdHelp=これは、WebAuthnリライングパーティーとしてのIDです。オリジンの有効なドメインでなければなりません。 @@ -315,7 +315,7 @@ canonicalizationHelp=XML署名の正規化方式。 sessions=セッション fullSyncPeriodHelp=完全同期の周期(秒)。 priority=優先度 -trustEmail=メールアドレスを信頼 +trustEmail=メールアドレスを信頼する jsonType.label=クレームJSONタイプ fullScopeAllowed=フルスコープを許可 syncModes.inherit=継承 @@ -358,7 +358,7 @@ protocol=プロトコル manageAccount=アカウントの管理 clientSecret=クライアントシークレット httpPostBindingAuthnRequest=AuthnRequestのHTTP-POSTバインディング -includeInAccessToken.label=アクセストークンに追加 +includeInAccessToken.label=アクセストークンへの追加 iconUri=アイコンURI usersInRole=ロールのユーザー groupsClaimHelp=定義されている場合、ポリシーは、パーミッションを要求するアイデンティティーを表すアクセストークンまたはIDトークン内の特定のクレームから、ユーザーのグループを取得します。定義されていない場合、ユーザーのグループはレルム設定から取得されます。 @@ -373,7 +373,7 @@ requestObjectSignatureAlgorithm=リクエストオブジェクトの署名アル tokenLifespan.expires=有効期限 mappers=マッパー waitIncrementSeconds=連続失敗時の待機時間 -name-id-format=Name IDフォーマット +name-id-format=NameIDフォーマット credentials=クレデンシャル webAuthnPolicyCreateTimeoutHelp=ユーザーの公開鍵クレデンシャルの作成に対するタイムアウト値(秒単位)。0に設定すると、このタイムアウトオプションは適応されません。 policyType.hotp=カウンターベース @@ -413,7 +413,7 @@ useKerberosForPasswordAuthentication=パスワード認証にKerberosを使用 debugHelp=Krb5LoginModuleの標準出力へのデバッグロギングの有効/無効を設定します。 validatorColNames.colConfig=設定 nodeHost=ノードホスト -quickLoginCheckMilliSeconds=クイックログインチェックのミリ秒 +quickLoginCheckMilliSeconds=クイックログインチェックの時間(ミリ秒) unspecified=未指定 profile=プロファイル active=アクティブ @@ -474,7 +474,7 @@ month=月 addressClaim.region.tooltip=「address」トークンクレーム内の「region」サブクレームにマップするために使用されるユーザー属性の名前。デフォルトは「region」です。 expiration=有効期限 logoutServicePostBindingURL=ログアウトサービスのPOSTバインディングURL -assertionConsumerServicePostBindingURLHelp=アサーションコンシューマーサービス(ログインレスポンス)のSAML POSTバインディングURLを設定します。このBindingのためのURLがない場合は空でよいです。 +assertionConsumerServicePostBindingURLHelp=アサーションコンシューマーサービス(ログインレスポンス)のSAML POSTバインディングURLを設定します。このバインディングのためのURLがない場合は空でよいです。 resourceTypes=リソースタイプ includeInUserInfo.label=UserInfoに追加 back=戻る @@ -681,8 +681,8 @@ keyPasswordHelp=秘密鍵のパスワード frontchannelLogout=フロントチャネルログアウト titleRoles=レルムロール frontendUrl=フロントエンドURL -sectorIdentifierUri.tooltip=pairwise sub値を使用し、かつ動的クライアント登録をサポートするプロバイダーは、sector_identifier_uriパラメーターを使用すべきです(SHOULD)。これは、共通の管理下にあるWebサイト群に対し、個々のドメイン名とは独立してparwise sub値の一貫性を保持する方法を提供します。また、クライアントに対し、すべてのユーザーを再登録させることなしにredirect_uriを変更する方法も提供します。 -rdnLdapAttribute=RDN LDAP属性 +sectorIdentifierUri.tooltip=ペアワイズsub値を使用し、かつ動的クライアント登録をサポートするプロバイダーは、sector_identifier_uriパラメーターを使用すべきです(SHOULD)。これは、共通の管理下にあるWebサイト群に対し、個々のドメイン名とは独立してペアワイズsub値の一貫性を保持する方法を提供します。また、クライアントに対し、すべてのユーザーを再登録させることなしにredirect_uriを変更する方法も提供します。 +rdnLdapAttribute=RDNのLDAP属性 replyToDisplayName=返信先の表示名 xRobotsTag=X-Robots-Tag bindType=バインドタイプ @@ -691,18 +691,18 @@ contextualInfo=コンテキスト情報 syncModeHelp=すべてのマッパーのデフォルトの同期モード。同期モードは、マッパーを使用してユーザーデータを同期するタイミングを決定します。可能な値は次のとおりです。このオプションが導入される前の動作を維持する「レガシー」、このアイデンティティープロバイダーを使用したユーザーの初回ログイン時に一度だけユーザーをインポートする「インポート」、このアイデンティティープロバイダーでログインするたびにユーザーを常に更新する「強制」。 applyPolicyHelp=このポリシーやパーミッションで定義されたスコープに適用するすべてのポリシーを設定します。 temporaryPassword=一時的 -sslType.none=none +sslType.none=無し clientsPermissionsHint=このクライアントを管理したり、このクライアントによって定義されたロールを適用したりする管理者のきめ細かいパーミッションです。 consentScreenText=同意画面のテキスト bruteForceDetection=ブルートフォースの検出 archiveFormatHelp=JavaキーストアまたはPKCS12アーカイブ形式。 xContentTypeOptions=X-Content-Type-Options keyAlias=キーエイリアス -none=none +none=無し type=タイプ seconds=秒 otpPolicyDigits=桁数 -ownerManagedAccess=User-Managed Accessの有効 +ownerManagedAccess=User-Managed Accessの有効化 permissions=パーミッション accountThemeHelp=ユーザーアカウント管理コンソールのテーマを選択します。 displayOnConsentScreenHelp=オンの場合、同意が必要なクライアントにこのクライアントスコープが追加されると、「同意画面のテキスト」で指定されたテキストが同意画面に表示されます。オフの場合、このクライアントスコープは同意画面に表示されません。 @@ -713,7 +713,7 @@ titleUsers=ユーザー scopePermissions.users.user-impersonated-description=どのユーザーに代理ログインするかを決定するポリシー。これらのポリシーは、代理ログインされたユーザーに適用されます。 forceAuthenticationHelp=アイデンティティープロバイダーが以前のセキュリティーコンテキストに頼るのではなく、プレゼンターを直接認証すべきかどうかを設定します。 testClusterAvailability=クラスターの可用性のテスト -forceNameIdFormat=Name IDフォーマットを強制 +forceNameIdFormat=NameIDフォーマットを強制 scopePermissions.users.manage-description=管理者がレルム内のすべてのユーザーを管理できるかどうかを決定するポリシー included.client.audience.tooltip=指定されたオーディエンスクライアントのクライアントIDが、トークンのオーディエンス(aud)フィールドに含まれます。トークンに既存のオーディエンスが存在する場合は、指定された値が単にそれらに追加されます。既存のオーディエンスを上書きすることはありません。 addRole=ロールの追加 @@ -1469,7 +1469,7 @@ inputBackgroundColor=入力背景色 inputTextColor=入力テキストカラー eventTypes.CLIENT_INFO.name=クライアント情報 chooseResources=インポートしたいリソースを選択してください。 -selectOne=オプションを選択する +selectOne=オプションの選択 noKeys=鍵がありません repeat=繰り返し downloadType=ダウンロードの種類についての情報です。 @@ -1509,7 +1509,7 @@ SSOSessionSettings=SSOセッションの設定 eventConfigSuccessfully=設定が正常に保存されました moveTo=移動 emptyEventsInstructions=追加できるイベントタイプはもうありません -selectATheme=テーマを選択する +selectATheme=テーマの選択 permissionsList=パーミッションリスト themeColors=テーマカラー defaults=デフォルトへのリセット @@ -1897,17 +1897,17 @@ userModelAttributeNameHelp=LDAPからユーザーをインポートするとき groupsDescription=グループとは、ユーザーに適用できる属性とロールマッピングのセットです。グループを作成、編集、削除したり、その子組織と親組織を管理したりできます。 protocolTypes.all=すべて executorTypeSelectHelpText=エグゼキュータータイプ選択ヘルプテキスト -addExecutorSuccess=成功!エグゼキューターが正常に作成されました +addExecutorSuccess=エグゼキューターが正常に作成されました onDragCancel=ドラッグはキャンセルされました。リストは変更されません。 removeUser=ユーザーの削除 -templateHelp=インポートするユーザー名のフォーマットに使用するテンプレート。置換は${}で囲みます。例: '${ALIAS}.${CLAIM.sub}'。ALIASはプロバイダーのエイリアスです。CLAIM.はIDまたはアクセストークンのクレームを参照します。置換後の値に|uppercaseまたは|lowercaseを付加することで、大文字または小文字に変換できます。例: '${CLAIM.sub | lowercase}'。 -privateKeyMask=PRIVATE KEY NOT SET UP OR KNOWN +templateHelp=インポートするユーザー名のフォーマットに使用するテンプレート。置換は${}で囲みます。例: 「${ALIAS}.${CLAIM.sub}」。ALIASはプロバイダーのエイリアスです。CLAIM.はIDまたはアクセストークンのクレームを参照します。置換後の値に|uppercaseまたは|lowercaseを付加することで、大文字または小文字に変換できます。例: 「${CLAIM.sub | lowercase}」。 +privateKeyMask=秘密鍵が設定されていないか不明です createUserProviderError=ユーザーフェデレーションプロバイダーを作成できませんでした: {{error}} eventTypes.USER_DISABLED_BY_PERMANENT_LOCKOUT.description=永続的なロックアウトによって無効にされるユーザー eventTypes.USER_DISABLED_BY_PERMANENT_LOCKOUT_ERROR.description=永続的なロックアウトエラーによって無効にされるユーザー eventTypes.USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR.description=一時的なロックアウトエラーによって無効なユーザー clientPoliciesProfilesHelpText=クライアントプロファイルを使用すると、クライアントで実行される様々なアクションを実行するエグゼキューターのセットを作成できます。アクションには、クライアントの作成や更新などの管理者アクションや、クライアントへの認証などのユーザーアクションが含まれます。 -eventTypes.USER_INFO_REQUEST_ERROR.name=UserInfoリクエストエラー +eventTypes.USER_INFO_REQUEST_ERROR.name=ユーザー情報リクエストエラー eventTypes.USER_DISABLED_BY_TEMPORARY_LOCKOUT.description=一時的なロックアウトによって無効なユーザー xContentTypeOptionsHelp=デフォルト値により、Internet ExplorerとGoogle Chromeは宣言されたコンテンツタイプとは異なる応答をMIMEスニッフィングできなくなります。<1>詳細 useDiscoveryEndpointHelp=この設定を有効にすると、プロバイダーの設定を取得するためにディスカバリーエンドポイントが使用されます。Keycloakはエンドポイントから設定を読み込み、ソースに更新があれば自動的に設定を更新します。 @@ -1938,7 +1938,7 @@ addKerberosWizardDescription=ここに説明文を入力してください。 eventTypes.CODE_TO_TOKEN.description=コードとトークンの交換 oAuthDevicePollingIntervalHelp=トークンエンドポイントへのポーリングリクエストの間でクライアントが待機する必要がある最小時間(秒)。 eventTypes.REVOKE_GRANT.description=付与の無効化 -addAuthnContextDeclRef=Add AuthnContext DeclRef +addAuthnContextDeclRef=認証コンテキスト宣言参照の追加 eventTypes.SEND_IDENTITY_PROVIDER_LINK.description=アイデンティティープロバイダーリンクの送信 dateTo=終了日付 mapperTypeGroupLdapMapperHelp=LDAPのDNグループのグループマッピングをKeycloakのグループマッピングにマッピングするために使用されます @@ -1951,9 +1951,9 @@ userInfo=ユーザー情報 emptyExecutionInstructions=このフローの定義は、サブフローまたはエグゼキューションを追加することで開始できます。 offlineSessionSettings=オフラインセッションの設定 unAssignRole=割り当て解除 -NONE=NONE +NONE=無し keystorePasswordHelp=キーストアのパスワード -keyPlaceholder=キーをタイプして下さい。 +keyPlaceholder=キーをタイプして下さい clientSettings=クライアントの詳細 tokenDeleteSuccess=初期アクセストークンが正常に削除されました。 clientUpdaterTrustedHostsTooltip=信頼されているホストのリスト。クライアントの登録リクエストまたは更新リクエストがこの設定で指定されたホスト/ドメインから送信された場合、条件は真と評価されます。ホスト名またはIPアドレスを使用できます。*.example.comのように先頭にアスタリスクを使用すると、ドメインexample.com全体が信頼されます。 @@ -2019,7 +2019,7 @@ ellipticCurve=楕円曲線 noRolesInstructions-clientScopes=このクライアントスコープにはロールが作成されていません。まずはロールを作成してください。 validateUserObjectClasses=1つ以上のユーザーオブジェクトクラスを入力する必要があります requiredForLabel.users=ユーザーのみ -notBeforeSuccess=成功!レルムに「Not before」を設定しました。 +notBeforeSuccess=レルムに「Not before」を設定しました。 quickLoginCheckMilliSecondsHelp=クイックログイン失敗があまりにも頻繁に発生した場合は、ユーザーをロックアウトします。 eventTypes.REMOVE_TOTP.name=TOTPの削除 userFedUnlinkUsersConfirm=すべてのユーザーのリンクを解除しますか?データベースにパスワードがないユーザーは認証できなくなります。 @@ -2048,7 +2048,7 @@ searchFor=ロール名でロールを検索 clientPoliciesSubTab=クライアントポリシーサブタブ writeOnly=書き込みのみ regexPatternHelp=正規表現パターンを指定します。 -noGroupsInThisSubGroup=このサブグループにはグループがありません。 +noGroupsInThisSubGroup=このサブグループにはグループがありません groupUpdated=グループを更新しました。 filenamePlaceholder=PEMファイルをアップロードするか、以下に鍵を貼り付けてください。 clientProfilesSubTab=クライアントプロファイルサブタブ @@ -2122,7 +2122,7 @@ cibaInterval=間隔 x509Certificate=X509証明書 createPolicySuccess=ポリシーが正常に作成されました。 encryptionKeysConfig=暗号化鍵の設定 -addRequestUri=有効なリクエストURIを追加 +addRequestUri=有効なリクエストURIの追加 ldapAttributeValue=LDAP属性値 emptyResources=リソースがありません。 ldapSynchronizationSettingsDescription=このセクションには、LDAPからKeycloakデータベースへのユーザーの同期に関連するオプションが含まれています。 @@ -2130,11 +2130,11 @@ importAdded_other={{count}}件のレコードが追加されました。 editCondition=条件の編集 attributeDisplayNameHelp=属性の表示名です。ローカライズされた値のキーもサポートしています。例: ${profile.attribute.phoneNumber}。翻訳を追加するには、「Globe Route」のアイコンをクリックしてください。 deleteValidatorConfirmMsg=バリデーター{{validatorName}}を完全に削除してもよいですか? -saveProviderError=プロバイダーの保存中にエラーが発生しました:{{error}} +saveProviderError=プロバイダーの保存中にエラーが発生しました: {{error}} createClientSuccess=クライアントが正常に作成されました usernameTemplateImporter=インポートするユーザー名をフォーマットします。 assignedType=割り当てられたタイプ -validateKeyTab=キータブを入力する必要があります。 +validateKeyTab=キータブを入力する必要があります preserveGroupInheritance=グループの継承の保持 createClientScopeSuccess=クライアントスコープが作成されました oAuthDeviceCodeLifespanHelp=デバイスコードとユーザーコードの有効期限が切れるまでの最大時間。この値は、ユーザーがセカンダリーデバイスを取得したり、検証URIにアクセスしたり、ログインしたりできるようにするために十分な長さである必要がありますが、フィッシングに使用されたコードの使用を制限するのに十分な短さである必要もあります。 @@ -2148,7 +2148,7 @@ passMaxAgeHelp=max_ageをアイデンティティープロバイダーに渡し ssoSessionMaxRememberMe=ユーザーが「Remember me」オプションを設定している場合、セッションが期限切れになるまでの最大時間です。セッションが期限切れになると、トークンとブラウザーセッションは無効になります。設定されていない場合は、標準のSSOセッション最大値が使用されます。 setPasswordConfirm=パスワードを設定しますか? skipped=スキップ -selectOrTypeAKey=キーを選択または入力する +selectOrTypeAKey=鍵の選択または入力 authorizationEncryptedResponseEnc=認可レスポンス暗号化コンテンツ暗号化アルゴリズム notVerified=未検証 oid4vcIssuerMetadata=OpenID4VCIクレデンシャル発行者のメタデータ @@ -2157,11 +2157,11 @@ validations=バリデーション deleteMessageBundleError=バンドルからメッセージを削除中にエラーが発生しました: {{error}} jwksUrlConfig=JWKS URLの設定 eventTypes.CLIENT_DELETE.description=クライアントの削除 -updatedRequiredActionSuccess=必要なアクションが正常に更新されました。 +updatedRequiredActionSuccess=必要なアクションが正常に更新されました providerCreateError={{error}}のためクライアントポリシーを作成できませんでした appliedByProviders=以下のプロバイダーによって適用されています。 saveEventListenersSuccess=イベントリスナーが更新されました。 -validatorDeletedError=ユーザープロファイルの保存中にエラーが発生しました:{{error}} +validatorDeletedError=ユーザープロファイルの保存中にエラーが発生しました: {{error}} removeAttribute=属性の削除 resourceDetails=リソースの詳細 times.years=年 @@ -2183,7 +2183,7 @@ keyTabHelp=サーバープリンシパルのクレデンシャルを含むKerber modeHelp=LDAP_ONLYは、ユーザーのすべてのグループマッピングがLDAPから取得され、LDAPに保存されることを意味します。READ_ONLYは読み取り専用LDAPモードで、グループマッピングはLDAPとデータベースの両方から取得され、マージされます。新しいグループへの参加はLDAPではなくデータベースに保存されます。IMPORTは読み取り専用LDAPモードで、ユーザーがLDAPからインポートされる際にグループマッピングがLDAPから取得され、ローカルのKeycloakデータベースに保存されます。 updateClientProfileSuccess=クライアントプロファイルが正常に更新されました noClientPoliciesInstructions=クライアントポリシーがありません。新しいクライアントポリシーを作成するには、「クライアントポリシーの作成」を選択してください。 -eventTypes.RESTART_AUTHENTICATION.description=認証を再開する +eventTypes.RESTART_AUTHENTICATION.description=認証の再開 exportFail=レルムをエクスポートできませんでした: {{error}} targetHelp=マッパーの宛先フィールド。LOCAL(デフォルト)は、ユーザーのインポート時にローカルデータベースに保存されているユーザー名に変更が適用されることを意味します。BROKER_IDとBROKER_USERNAMEは、それぞれ連携されるユーザーの検索に使用されるIDまたはユーザー名に変更が保存されることを意味します。 forgotPasswordHelpText=クレデンシャルを忘れたユーザーのために、ログインページにリンクを表示します。 @@ -2192,7 +2192,7 @@ finish=終了 ldapFilterHelp=LDAPフィルターは、LDAPグループを取得するためのクエリー全体にカスタムフィルターを追加します。追加のフィルタリングが不要で、LDAPからすべてのグループを取得する場合は、このフィールドを空のままにしてください。それ以外の場合は、フィルターが「(」で始まり「)」で終わるようにしてください。 deleteConfirmRealmSetting=レルムを削除すると、関連付けられているすべてのデータも削除されます。 eventTypes.UPDATE_PASSWORD_ERROR.description=パスワードの更新エラー -roleHelp=すべての属性が揃っている場合にユーザーに付与するロールです。「ロールを選択」をクリックしてロールを参照するか、テキストボックスにロールを入力してください。クライアントロールを参照するには、clientname.clientroleという構文を使用します(例:myclient.myrole)。 +roleHelp=すべての属性が揃っている場合にユーザーに付与するロールです。「ロールの選択」をクリックしてロールを参照するか、テキストボックスにロールを入力してください。クライアントロールを参照するには、clientname.clientroleという構文を使用します(例:myclient.myrole)。 choose=選択... updatedRequiredActionError=必要なアクションを更新できませんでした: {{error}} evaluateExplain=このページでは、すべてのプロトコルマッパーとロールスコープマッピングを確認できます @@ -2204,13 +2204,13 @@ emptyClientScopesInstructions=現在、このクライアントにはクライ groupDeleteError=グループの削除中にエラーが発生しました: {{error}} usersDNHelp=ユーザーが存在するLDAPツリーの完全なDN。このDNはLDAPユーザーの親DNです。例えば、ユーザーのDNが「uid='john',ou=users,dc=example,dc=com」であると仮定すると、「ou=users,dc=example,dc=com」のようになります。 identityProviderEntityIdHelp=受信したSAMLアサーションの発行者の検証に使用されるエンティティーID。空の場合、発行者の検証は実行されません。 -noRoles-client=このクライアントにはロールがありません。 +noRoles-client=このクライアントにはロールがありません selectGroups=参加するグループの選択 eventTypes.IDENTITY_PROVIDER_LOGIN.name=アイデンティティープロバイダーログイン -eventTypes.REMOVE_FEDERATED_IDENTITY.description=連携されたアイデンティティーを削除する +eventTypes.REMOVE_FEDERATED_IDENTITY.description=連携されたアイデンティティーの削除 mappedGroupAttributes=マッピングされたグループ属性 -exportAuthDetailsError=認可の詳細のエクスポート中にエラーが発生しました:{{error}} -clientOfflineSessionIdleHelp=クライアントのオフラインセッションがアイドル状態のままでいられる時間。この時間を超えると、クライアントオフラインセッションは無効になります。このオプションは、グローバルユーザーのSSOセッションには影響しません。設定されていない場合は、レルムの「オフラインセッションアイドル」の値が使用されます。 +exportAuthDetailsError=認可の詳細のエクスポート中にエラーが発生しました: {{error}} +clientOfflineSessionIdleHelp=クライアントオフラインセッションがアイドル状態のままでいられる時間。この時間を超えると、クライアントオフラインセッションは無効になります。このオプションは、グローバルユーザーのSSOセッションには影響しません。設定されていない場合は、レルムの「オフラインセッションアイドル」の値が使用されます。 userDetails=ユーザーの詳細 inputTypeStep=入力ステップサイズ useRefreshTokenForClientCredentialsGrant=クライアントクレデンシャルグラントにリフレッシュトークンを使用する @@ -2334,7 +2334,7 @@ updateClientPoliciesError=クライアントポリシーを更新できません requestObjectEncodingHelp=「request」または「request_uri」のパラメーターで指定されたOIDCリクエストオブジェクトのコンテンツを暗号化する際に、クライアントが使用するJWEアルゴリズム。「any」に設定すると、任意のアルゴリズムが許可されます。 confirmAccessTokenTitle=登録アクセストークンを再生成しますか? ldapGeneralOptionsSettingsDescription=このセクションには、すべてのユーザーストレージプロバイダーに共通するいくつかの基本オプションが含まれています。 -tokenLifespan.inherited=レルム設定から継承する +tokenLifespan.inherited=レルム設定からの継承 addTranslationError=翻訳の作成中にエラーが発生しました: {{error}} addProvider=プロバイダーの追加 readOnlyHelp=LDAPからUserModelにインポートされるが、Keycloakでユーザーが更新されたときにLDAPに保存されない読み取り専用属性。 @@ -2408,9 +2408,9 @@ roleImportSuccess=ロールのインポートに成功しました allowed-client-scopes.label=許可されたクライアントスコープ assignedClientScope=割り当てられたクライアントスコープ oneLevel=1階層 -notBeforeClearedSuccess=成功しました!レルムの「Not Before」がクリアされました。 -policySaveError=ポリシーを更新できませんでした:{{error}} -standardFlowHelp=これにより、認可コードを使用した標準的なOpenID Connectリダイレクトベースの認証が可能になります。OpenID ConnectまたはOAuth2仕様の観点から言えば、このクライアントで「認可コードフロー」のサポートが可能になります。 +notBeforeClearedSuccess=レルムの「Not Before」がクリアされました。 +policySaveError=ポリシーを更新できませんでした: {{error}} +standardFlowHelp=これにより、認可コードを使用した標準的なOpenID Connectのリダイレクトベースの認証が可能になります。OpenID ConnectまたはOAuth 2の仕様の観点から言えば、このクライアントで「認可コードフロー」のサポートが可能になります。 parentId=親ID rsa=rsa clientScopeType.none=なし @@ -2441,26 +2441,26 @@ alwaysReadValueFromLdap=常にLDAPから値を読み取る generatedUserInfoNo=ユーザー情報は生成されませんでした。 impersonateConfirmDialog=このユーザーとしてログインしてもよろしいですか?このユーザーが同じレルム内に存在する場合、このユーザーとしてログインする前に、現在のログインセッションがログアウトされます。 eventTypes.IDENTITY_PROVIDER_POST_LOGIN.name=アインデンティティープロバイダーログイン後処理 -groupCreated=グループを作成しました。 -generateError=新しい鍵ペアと証明書を生成できませんでした:{{error}} +groupCreated=グループを作成しました +generateError=新しい鍵ペアと証明書を生成できませんでした: {{error}} federationLinkHelp=このローカルに保存されたユーザーのインポート元であるUserStorageProvider。 reqAuthnConstraints=要求されたAuthnContextの制約 deleteAttributeText=属性の削除 derFormattedHelp=証明書がLDAPのDER形式であり、PEM形式ではない場合にこれを有効にします。 deleteResourceConfirm=このリソースを削除すると、一部のパーミッションに影響が出ます。 whoWillAppearPopoverTextRoles=このタブには、このロールに直接割り当てられているユーザーのみが表示されます。関連するロールまたはグループを通じて、このロールが割り当てられているユーザーを確認するには、以下に移動します -updateFlowSuccess=フローが正常に更新されました。 +updateFlowSuccess=フローが正常に更新されました eventTypes.CUSTOM_REQUIRED_ACTION_ERROR.name=カスタム必須アクションエラー noSessionsForUser=現在、このユーザーのアクティブなセッションはありません。 realmExplain=レルムは、ユーザー、クレデンシャル、ロール、およびグループのセットを管理します。ユーザーはレルムに属し、レルムにログインします。レルムは互いに分離されており、制御対象のユーザーのみを管理および認証できます。 evaluateError=次の理由により評価できませんでした:{{error}} roleRemoveAssociatedRoleConfirm=関連付けられているロールを削除しますか? -samlKeysExportSuccess=鍵のエクスポートに成功しました。 +samlKeysExportSuccess=鍵のエクスポートに成功しました policyProvider.group=1 つ以上のグループ(およびその階層)のセットがオブジェクトにアクセスできるようにするパーミッションの条件を定義します。 updatedUserProfileError=ユーザープロファイルの設定が保存されていません:{{error}} policyProvider.time=パーミッションの時間条件を定義します。 -nameIdPolicyFormatHelp=名前識別子形式に対応するURI参照を指定します。 -deleteFlowError=フローを削除できませんでした:{{error}} +nameIdPolicyFormatHelp=NameIDフォーマットに対応するURI参照を指定します。 +deleteFlowError=フローを削除できませんでした: {{error}} alwaysReadValueFromLdapHelp=オンの場合、LDAP属性の読み取り中に、Keycloakデータベースの値の代わりに常にこの値が使用されます。 xFrameOptionsHelp=デフォルト値は、非オリジンのiframeによってページが含まれないようにします。<1>詳細 sync-ldap-groups-to-keycloak=LDAPグループをKeycloakに同期する @@ -2486,7 +2486,7 @@ residentKey.not\ specified=指定がありません。 mappersList=マッパー一覧 importError=ユーザーのDNを入力する必要があります。 permissionsScopeName=スコープ名 -emptyPermissions=パーミッションがありません。 +emptyPermissions=パーミッションがありません userCreated=ユーザーが作成されました。 noUserDetails=ユーザーの詳細はありません。 inputTypeRows=入力行 @@ -2495,7 +2495,7 @@ tokenDeleteError=初期アクセストークンを削除できませんでした CONFIGURE_TOTP=OTP の設定(CONFIGURE_TOTP) whoWillAppearLinkTextRoles=この一覧に表示されるユーザー targetClaim=対象のクレーム -deleteAttributeGroupError=ユーザー属性グループを削除できませんでした:{{error}} +deleteAttributeGroupError=ユーザー属性グループを削除できませんでした: {{error}} selectIfResourceExists=リソースがすでに存在する場合は、何をすべきかを指定します。 passwordPoliciesHelp.notEmail=パスワードはユーザーの電子メール アドレスと同じ値にできません。 userSessionAttribute=ユーザーセッション属性 @@ -2503,7 +2503,7 @@ samlSettings=SAML設定 mapperTypeHardcodedLdapRoleMapperHelp=LDAPからインポートされたユーザーは、この設定されたロールに自動的に追加されます。 iconUriHelp=アイコンを指すURI。 cibaBackchannelTokenDeliveryMode=バックチャネルトークンデリバリーモード -validateEditMode=編集モードを選択する必要があります。 +validateEditMode=編集モードを選択する必要があります eventTypes.UPDATE_PROFILE.name=プロファイルの更新 isMandatoryInLdap=LDAPでは必須 deleteClientConditionError=条件の作成中にエラーが発生しました:{{error}} @@ -2520,8 +2520,8 @@ importResourceError={{error}}のためリソースをインポートできませ regexAttributeValuesHelp=有効にすると、属性値は正規表現として解釈されます。 allowRegexComparison=正規表現パターンの比較を許可する eventTypes.IMPERSONATE_ERROR.name=代理ログインエラー -valuePlaceholder=値を入力してください。 -roleImportError=ロールをインポートできませんでした。 +valuePlaceholder=値を入力してください +roleImportError=ロールをインポートできませんでした overallResults=全体的な結果 createClientPolicySuccess=新しいポリシーが作成されました。 dynamicScope=動的スコープ @@ -2532,14 +2532,14 @@ testConnectionSuccess=SMTP接続に成功しました。電子メールが送信 noProvidersLinked=アイデンティティープロバイダーがリンクされていません。 passwordPoliciesHelp.maxAuthAge=再認証なしでパスワードを変更できる認証の最大有効期間。 resultPermit=結果 - 許可 -deletePermission=パーミッションがありません。 -updateFlowError=フローを更新できませんでした:{{error}} +deletePermission=パーミッションを永久に削除しますか? +updateFlowError=フローを更新できませんでした: {{error}} updateClientContext=クライアントコンテキストの更新 -removeAssociatedRoles=関連付けられているロールを削除する +removeAssociatedRoles=関連付けられているロールの削除 eventTypes.IDENTITY_PROVIDER_POST_LOGIN.description=アインデンティティープロバイダーログイン後処理 -scopesAsRequested=スコープが要求されます。 -validateUuidLDAPAttribute=UUIDのLDAP属性を入力する必要があります。 -createResourceBasedPermission=リソースベースのパーミッションを作成する +scopesAsRequested=スコープが要求されます +validateUuidLDAPAttribute=UUIDのLDAP属性を入力する必要があります +createResourceBasedPermission=リソースベースのパーミッションの作成 ldapRolesDn=LDAPロールDN importOverwritten_one=1つのレコードが上書きされました。 samlEntityDescriptorHelp=外部IDPメタデータを設定ファイルよりロード、またはURLよりダウンロードして設定します。 @@ -2567,21 +2567,21 @@ authenticatedAccessPolicies=認証されたアクセスポリシー signatureAndEncryption=署名と暗号化 reset=リセット conditionType=条件のタイプ -importConfigHelp=ダウンロードしたIDP発見記述子からメタデータをインポートします。 +importConfigHelp=ダウンロードしたIDPディスカバリー記述子からメタデータをインポートします。 moveGroupError=グループを移動できませんでした:{{error}} executorType=エグゼキューターのタイプ noDefaultGroups=デフォルトグループがありません。 deleteConfirmIdentityProvider=プロバイダー「{{provider}}」を完全に削除してもよろしいですか? eventTypes.CLIENT_INFO_ERROR.description=クライアント情報エラー anonymousAccessPolicies=匿名アクセスポリシー -updateErrorClientScope=クライアントスコープを更新できませんでした:{{error}} +updateErrorClientScope=クライアントスコープを更新できませんでした: {{error}} deleteClientConditionSuccess=条件が正常に削除されました。 fullNameLdapReadOnlyHelp=読み取り専用の場合、データはLDAPからKeycloakのデータベースにインポートされますが、Keycloakでユーザーが更新されてもLDAPに保存されません。 generalSettings=一般的な設定 addClientProfileError=クライアントプロファイルを作成できませんでした:{{error}} client-roles.label=クライアントロール keysFilter.PASSIVE=パッシブキー -copyFlowSuccess=フローが正常に複製されました。 +copyFlowSuccess=フローが正常に複製されました noRoles=このユーザーにはロールがありません eventTypes.PERMISSION_TOKEN_ERROR.description=パーミッショントークンエラー couldNotLinkIdP=アイデンティティープロバイダーをリンクできませんでした:{{error}} @@ -2602,7 +2602,7 @@ inputHelperTextBefore=入力フィールドのヘルパーテキスト(上) validateUsersDn=ユーザーのDNを入力する必要があります。 validateAttributeName=名前のない属性設定は許可されません。 noRolesInstructions-groups=このグループにはまだロールが作成されていません。開始するにはロールを作成してください。 -emptyAuthorizationScopes=認可スコープがありません。 +emptyAuthorizationScopes=認可スコープがありません userFederationExplain=ユーザーフェデレーションは、LDAPやActive Directoryなどの外部データベースおよびディレクトリーへのアクセスを提供します。 noRoles-clientScopes=このクライアントスコープにはロールがありません。 copyError=認可の詳細をコピー中にエラーが発生しました:{{error}} @@ -2619,20 +2619,20 @@ validatePasswordPolicyHelp=Keycloakがパスワードを更新する前に、レ dragHelp=スペースキーまたはEnterキーをクリックしてドラッグを開始し、矢印キーで上下に移動します。ドラッグを確定するにはEnterキーを押します。ドラッグ操作をキャンセルするには、他のキーをクリックします。 validPostLogoutRedirectURIsHelp=ログアウト成功後にブラウザーがリダイレクトできる有効なURIパターン。「+」または空欄の場合は、有効なリダイレクトURIのリストが使用されます。「-」の場合は、ログアウト後のリダイレクトURIは許可されません。「http://example.com/*」のような単純なワイルドカードを使用できます。/my/relative/path/*のような相対パスも指定できます。相対パスはクライアントのルートURLを基準とします。ルートURLが指定されていない場合は、認証サーバーのルートURLが使用されます。 bruteForceModeHelpText=有効にした場合、ブルートフォース攻撃が検出された場合にユーザーアカウントに対して何が起こるかを指定します。 -createResourceSuccess=リソースが正常に作成されました。 -clientSecretError=次の理由によりクライアント シークレットを再生成できませんでした:{{error}} +createResourceSuccess=リソースが正常に作成されました +clientSecretError=次の理由によりクライアント シークレットを再生成できませんでした: {{error}} cibaExpiresInHelp=認証リクエストを受信してからの「auth_req_id」の有効期限(秒)。 -copyFlowError=フローを複製できませんでした:{{error}} +copyFlowError=フローを複製できませんでした: {{error}} oauthDeviceAuthorizationGrant=OAuth 2.0デバイス認可グラント eventTypes.OAUTH2_DEVICE_VERIFY_USER_CODE.name=OAuth2デバイス検証ユーザーコード attestationPreference.not\ specified=未指定 claimToRole=クレームが存在する場合は、指定されたレルムロールまたはクライアント ロールをユーザーに付与します。 -usersLeft_other={{count}} がグループを脱退しました。 +usersLeft_other={{count}} がグループを脱退しました user-events-cleared-error=ユーザーイベントをクリアできませんでした:{{error}} clientUpdaterSourceHost=条件は、クライアントを作成/更新しようとするエンティティーのホスト/ドメインをチェックして、ポリシーが適用されるかどうかを判断します。 generatedIdTokenHelp=選択されたユーザーが認証されると生成され、クライアントに送信されるIDトークンの例を表示します。有効なプロトコルマッパーとロールスコープマッピング、そして実際のユーザーに割り当てられたクレームとロールに基づいて、トークンに含まれるクレームとロールを確認できます。 -signature-algorithm=JWAアルゴリズム。クライアントが認証のためにJWTに署名する際に使用するアルゴリズムです。空白のままにした場合、クライアントは特定のクライアント認証子に適した任意のアルゴリズムを使用できます。 -useLowerCaseBearerTypeHelp=これがオンの場合、トークンレスポンスには小文字の「bearer」型が設定されます。デフォルトでは、サーバーはRFC6750で定義されている「Bearer」型を設定します。 +signature-algorithm=JWAアルゴリズム。クライアントが認証のためにJWTに署名する際に使用するアルゴリズムです。空白のままにした場合、クライアントは特定のクライアントオーセンティケーターに適した任意のアルゴリズムを使用できます。 +useLowerCaseBearerTypeHelp=オンの場合、トークンレスポンスには小文字の「bearer」型が設定されます。デフォルトでは、サーバーはRFC6750で定義されている「Bearer」型を設定します。 useRfc9068AccessTokenTypeHelp=有効にすると、アクセストークンのヘッダータイプはRFC9068に準拠した「at+jwt」になります。有効にしない場合は、「JWT」になります。 clientUpdaterSourceGroups=グループ eventTypes.EXECUTE_ACTIONS_ERROR.description=アクション実行時のエラー @@ -2641,7 +2641,7 @@ searchForProvider=プロバイダーの検索 isMandatoryInLdapHelp=trueの場合、この属性はLDAPで必須です。Keycloakのデータベースに値が存在しない場合は、空の値がLDAPに伝播されます。 addIdentityProvider={{provider}}プロバイダーの追加 ignoreMissingGroupsHelp=グループ階層内の欠落しているグループを無視します。 -eventTypes.INVALID_SIGNATURE.description=無効な署名です。 +eventTypes.INVALID_SIGNATURE.description=無効な署名です requiredWhen=必要な場合 scopePermissions.clients.configure-description=管理者の管理権限が制限されています。スコープ、テンプレート、プロトコルマッパーを設定できません。 chooseAPolicyTypeInstructions=以下の一覧からポリシータイプを1つ選択すると、新しい認可ポリシーを設定できます。いくつかのタイプと説明があります。 @@ -2658,13 +2658,13 @@ updateExecutorError=エグゼキューターが更新されませんでした: { nameField=名前 moveGroupSuccess=グループを移動しました attributesGroup=属性グループ -clientDeleteError=クライアントを削除できません:{{error}} +clientDeleteError=クライアントを削除できません: {{error}} idpAccountEmailVerification=IdPアカウントのメール検証 deleteExecutionSuccess=エグゼキューションが正常に削除されました exportSamlKeyTitle=SAML鍵のエクスポート groupDeleted_other=グループを削除しました addSubFlowHelp=サブフローは、汎用フローまたはフォームのいずれかです。フォームタイプは、ユーザーに対して単一のフローを生成するサブフローを構築するために使用されます。サブフローは、含まれるエグゼキューションの評価に応じて成功と評価される特別なタイプのエグゼキューションです。 -searchForFlow=フローを検索する +searchForFlow=フローの検索 addAuthnContextClassRef=認証コンテキストクラス参照を追加する authorizationSignedResponseAlgHelp=レスポンスモードが jwt の場合に、認可レスポンストークンの署名に使用されるJWAアルゴリズム。 fullNameLdapWriteOnlyHelp=書き込み専用の場合、Keycloakでユーザーが作成または更新されると、データがLDAPに伝播されます。ただし、このマッパーはLDAPからKeycloakへのデータの伝播には使用されません。この設定は、firstNameとlastNameの属性マッパーを別々に設定し、それらを使用してLDAPからKeycloakに属性を読み取りたい場合に便利です。 @@ -2696,7 +2696,7 @@ admin-events-cleared=管理者イベントはクリアされました AESKeySizeHelp=生成されるAES鍵のサイズ(バイト単位)。AES-128の場合はサイズ16、AES-192の場合はサイズ24、AES-256の場合はサイズ32です。警告: 一部のJDK実装では、128より大きい鍵は許可されません。 deleteProvider=プロバイダーを削除しますか? confirmAccessTokenBody=登録アクセストークンを再生成すると、クライアント登録サービスに関するアクセスデータが更新されます。 -updatePasswordPolicyError=パスワードポリシーを更新できませんでした:{{error}} +updatePasswordPolicyError=パスワードポリシーを更新できませんでした: {{error}} uuidLdapAttributeHelp=LDAP属性の名前。LDAP内のオブジェクトの一意のオブジェクト識別子(UUID)として使用されます。多くのLDAPサーバーベンダーでは「entryUUID」ですが、ベンダーによっては異なる場合もあります。例えば、Active Directoryの場合は「objectGUID」です。LDAPサーバーがUUIDの概念をサポートしていない場合は、ツリー内のLDAPユーザー間で一意であると想定される他の属性を使用できます。例えば、「uid」や「entryDN」などです。 validPostLogoutRedirectURIs=ログアウト成功後にブラウザーがリダイレクトできる有効なURIパターン。値が「+」または空欄の場合、有効なリダイレクトURIのリストが使用されます。値が「-」の場合、ログアウト後のリダイレクトURIは許可されません。「http://example.com/*」のような単純なワイルドカードを使用できます。「/my/relative/path/*」のような相対パスも指定できます。相対パスはクライアントのルートURLを基準とします。ルートURLが指定されていない場合は、認証サーバーのルートURLが使用されます。 authorizationScopeDetails=認可スコープの詳細 @@ -2735,7 +2735,7 @@ roleNameLdapAttributeHelp=ロールオブジェクトでロール名とRDNとし origin=オリジン addCondition=条件を追加 updateSuccess=プロバイダーは正常に更新されました -mappingDeletedSuccess=マッピングが正常に削除されました。 +mappingDeletedSuccess=マッピングが正常に削除されました noUsersFound=ユーザーが見つかりません。 mapperTypeGroupLdapMapper=group-ldap-mapper targetContextAttributes=対象コンテキスト属性 @@ -2752,7 +2752,7 @@ noUsersFoundError={{error}}のためユーザーが見つかりませんでし logoutServiceArtifactBindingUrl=ログアウトサービスアーティファクトバインディングURL deleteExecutorProfileConfirmTitle=エグゼキューターを削除しますか? createAttributeSuccess=成功しました。ユーザープロファイルの設定が保存されました。 -eventTypes.GRANT_CONSENT_ERROR.description=同意付与エラー +eventTypes.GRANT_CONSENT_ERROR.description=同意の付与エラー emptyResourcesInstructions=リソースを作成したい場合は、下のボタンをクリックしてください。 recommendedSsoTimeout=この値は、SSOセッションアイドルタイムアウトよりも短くすることをお勧めします: {{time}} policyProvider.aggregate=既存のポリシーを再利用してより複雑なポリシーを構築し、認可リクエストの処理中に評価されるポリシーからパーミッションをさらに切り離します。 @@ -2770,7 +2770,7 @@ updatePolicySuccess=ポリシーが正常に更新されました subjectHelp=クライアント証明書のサブジェクトDNを検証するための正規表現。あらゆる種類の表現に一致させるには、「(.*?)(?:$)」を使用します。 eventTypes.CUSTOM_REQUIRED_ACTION.name=カスタム必須アクション roleID=ロールID -filteredByClaim=必須クレームを検証する +filteredByClaim=必須クレームの検証 eventTypes.GRANT_CONSENT.description=同意の付与 eventTypes.OAUTH2_DEVICE_AUTH_ERROR.name=OAuth2デバイス認証エラー clientProfilesHelpItem=クライアントプロファイルのヘルプ項目 @@ -2786,8 +2786,8 @@ noSearchResults=検索結果がありません clientRegisterPolicyDeleteConfirm=クライアント登録ポリシー{{name}}を完全に削除してもよいですか? addAttribute={{label}}の追加 hide=隠す -rowCancelBtnAriaLabel={{messageBundle}}の編集をキャンセルする -associatedRolesRemoved=関連するロールが削除されました。 +rowCancelBtnAriaLabel={{messageBundle}}の編集のキャンセル +associatedRolesRemoved=関連するロールが削除されました targetContextAttributesHelp=アイデンティティー属性の代わりにコンテキスト属性(クレーム)の評価を定義します。 assign=割り当て inputOptionLabelsI18nPrefix=国際化キープレフィックス @@ -2816,7 +2816,7 @@ nameHelp=新しいフローの名前のヘルプテキスト resourcesToImport=インポートするリソース changeAuthenticatorConfirmTitle={{clientAuthenticatorType}}に変更しますか? userSessionAttributeValue=ユーザーセッション属性値 -joinGroupsFor=ユーザー{{username}}のグループに参加する +joinGroupsFor=ユーザー{{username}}のグループへの参加 attributeConsumingServiceIndex=属性消費サービスインデックス enabledHelp=鍵が有効かどうかを設定します eventTypes.IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR.description=アイデンティティープロバイダーからのトークン取得エラー @@ -2862,8 +2862,8 @@ groupsPermissionsHint=このロールの管理において、きめ細かいパ enableLdapv3PasswordHelp=LDAPv3パスワード変更拡張操作(RFC-3062)を使用します。パスワード変更拡張操作では通常、LDAPユーザーがLDAPサーバーに既にパスワードを持っていることが条件となります。そのため、これを「登録同期」と併用する場合は、ランダムに生成された初期パスワードを含む「ハードコードされたLDAP属性マッパー」も追加すると効果的です。 privateRSAKeyHelp=PEM形式でエンコードされた秘密RSA鍵 client-updater-trusted-hosts.tooltip=信頼するホストのリスト。クライアントの登録/更新リクエストがこの設定で指定されたホスト/ドメインから送信された場合、条件は真と評価されます。ホスト名またはIPアドレスを使用できます。先頭にアスタリスク(*.example.com)を付けると、ドメインexample.com全体が信頼されます。 -clientOfflineSessionMaxHelp=クライアントのオフライン・セッションが期限切れになるまでの最大時間。レルムレベルで「オフライン・セッションの最大制限」が有効になっている場合、クライアントのオフライン・セッションが期限切れになるとオフライン・トークンが無効化されます。このオプションはグローバル・ユーザーSSOセッションには影響しません。設定されていない場合は、レルムの「オフライン・セッションの最大制限」値が使用されます。 -clientAccessType=クライアントのアクセスタイプ(confidential、public、bearer-only)に基づいて、ポリシーを適用するかどうかを判断します。この条件は、ほとんどのOpenID Connectリクエスト(認可リクエスト、トークン・リクエスト、イントロスペクション・リクエストなど)でチェックされます。confidentialタイプのクライアントはクライアント認証を有効にしていますが、publicタイプのクライアントはクライアント認証を無効にしています。bearer-onlyは非推奨のクライアントタイプです。 +clientOfflineSessionMaxHelp=クライアントのオフラインセッションが期限切れになるまでの最大時間。レルムレベルで「オフラインセッションの最大制限」が有効になっている場合、クライアントのオフラインセッションが期限切れになるとオフライントークンが無効化されます。このオプションはグローバルユーザーSSOセッションには影響しません。設定されていない場合は、レルムの「オフラインセッションの最大制限」値が使用されます。 +clientAccessType=クライアントのアクセスタイプ(confidential、public、bearer-only)に基づいて、ポリシーを適用するかどうかを判断します。この条件は、ほとんどのOpenID Connectリクエスト(認可リクエスト、トークンリクエスト、イントロスペクションリクエストなど)でチェックされます。confidentialタイプのクライアントはクライアント認証を有効にしていますが、publicタイプのクライアントはクライアント認証を無効にしています。bearer-onlyは非推奨のクライアントタイプです。 top-level-flow-type.client-flow=クライアントフロー clientSecretSuccess=クライアントシークレットが再生成されました oAuthDeviceCodeLifespan=OAuth 2.0デバイスコードの有効期間 @@ -2898,41 +2898,41 @@ disableSigning=「{{key}}」を無効にする kc.client.user_agent=クライアント/ユーザーエージェント javaKeystore=java-keystore deleteProviderMapper=マッパーを削除しますか? -lookAroundHelp=トークン・ジェネレーターとサーバーが時間同期または回数同期していない場合に備えて、サーバーはどの程度の範囲(トークンの期間または回数)の誤差を許容する必要があるか。 +lookAroundHelp=トークンジェネレーターとサーバーが時間同期または回数同期していない場合に備えて、サーバーが許容する必要がある誤差の範囲(トークンの期間または回数)。 authorizationEncryptedResponseEncHelp=JWAレスポンスモードがjwtの場合、認可レスポンスの暗号化におけるコンテンツ暗号化に使用されるアルゴリズム。このオプションは、認可レスポンスを暗号化する場合に必要です。空のままにした場合、認可レスポンスは署名のみされ、暗号化されません。 fullName={{givenName}} {{familyName}} deletePolicyConfirm=このポリシーを削除すると、一部のパーミッションまたは集約されたポリシーに影響します。 AESKeySize=AES鍵のサイズ -postBrokerLoginFlowAliasHelp=このアイデンティティー・プロバイダーでログインするたびにトリガーされる認証フローのエイリアスです。このアイデンティティー・プロバイダーで認証された各ユーザーに対して追加の検証(例えばOTP)が必要な場合に便利です。このアイデンティティー・プロバイダーでのログイン後に追加の認証をトリガーする必要がない場合は、「なし」のままにしてください。また、認証の実装では、ユーザーがアイデンティティー・プロバイダーによって既に設定されているため、ClientSessionに既に設定されていることを前提とする必要があることに注意してください。 -updatedUserProfileSuccess=ユーザー・プロファイルの設定が保存されました。 +postBrokerLoginFlowAliasHelp=このアイデンティティープロバイダーでログインするたびにトリガーされる認証フローのエイリアスです。このアイデンティティープロバイダーで認証された各ユーザーに対して追加の検証(例えばOTP)が必要な場合に便利です。このアイデンティティープロバイダーでのログイン後に追加の認証をトリガーする必要がない場合は、「なし」のままにしてください。また、認証の実装では、ユーザーがアイデンティティープロバイダーによって既に設定されているため、ClientSessionに既に設定されていることを前提とする必要があることに注意してください。 +updatedUserProfileSuccess=ユーザープロファイルの設定が保存されました leaveGroupConfirmDialog_one={{username}}をグループ{{groupname}}から削除してもよろしいですか? noRolesInstructions=このユーザーにはロールが割り当てられていません。まずはロールを割り当ててください。 clientRegisterPolicyDeleteConfirmTitle=クライアント登録ポリシーを削除しますか? permissionDeletedSuccess=パーミッションを削除しました。 -clientScopeRemoveSuccess=スコープ・マッピングが正常に削除されました。 -userCreateError=ユーザーを作成できませんでした:{{error}} -selectRealm=レルムを選択する -sync-keycloak-groups-to-ldap=KeycloakのグループをLDAPに同期する -deleteConfirm=プロバイダー「{{provider}}」を完全に削除してもよろしいですか? -compositesRemovedAlertDescription=関連するすべてのロールが削除されました。 +clientScopeRemoveSuccess=スコープマッピングが正常に削除されました。 +userCreateError=ユーザーを作成できませんでした: {{error}} +selectRealm=レルムの選択 +sync-keycloak-groups-to-ldap=KeycloakのグループのLDAPへの同期 +deleteConfirm=プロバイダー「{{provider}}」を完全に削除してもよいですか? +compositesRemovedAlertDescription=関連するすべてのロールが削除されました allowKerberosAuthenticationHelp=SPNEGO/Kerberosトークンを使用したユーザーのHTTP認証を有効/無効にします。認証されたユーザーに関するデータは、このLDAPサーバーからプロビジョニングされます。 importOverwritten_other={{count}}件のレコードが上書きされました。 doNotStoreUsersHelp=有効にすると、このブローカーのユーザーは内部データベースに保存されません。 eventTypes.UNREGISTER_NODE_ERROR.name=ノード登録解除エラー oauthDeviceAuthorizationGrantHelp=これにより、OAuth 2.0デバイス認可付与のサポートが有効になります。つまり、クライアントは、入力機能が制限されているか、適切なブラウザーがないデバイス上のアプリケーションになります。 groupDetails=グループの詳細 -roleNameLdapAttribute=ロール名LDAP属性 +roleNameLdapAttribute=ロール名のLDAP属性 unexpectedError=予期しないエラーが発生しました:{{error}} requirements.CONDITIONAL=条件付き -defaultGroupAdded_one=デフォルト・グループに新しいグループが追加されました。 -user-events-cleared=ユーザー・イベントはクリアされました。 +defaultGroupAdded_one=デフォルトグループに新しいグループが追加されました。 +user-events-cleared=ユーザーイベントはクリアされました usersLeft_one={{count}}人がグループを脱隊しました。 emailAsUsernameHelpText=ユーザーが電子メールをユーザー名として設定できるようにします。 -saveError=ユーザー・フェデレーション・プロバイダーを保存できませんでした:{{error}} +saveError=ユーザーフェデレーションプロバイダーを保存できませんでした: {{error}} eventTypes.OAUTH2_DEVICE_CODE_TO_TOKEN.name=OAuth2のデバイスコードとトークンの交換 -impersonateError=ユーザーになりすますことができませんでした:{{error}} -periodicFullSyncHelp=LDAPユーザーとKeycloakの定期的な完全同期を有効にするかどうか。 -createError=アイデンティティー・プロバイダーを作成できません: {{error}} +impersonateError=ユーザーになりすますことができませんでした: {{error}} +periodicFullSyncHelp=LDAPユーザーとKeycloakの定期的な完全同期を有効にするかどうか +createError=アイデンティティープロバイダーを作成できません: {{error}} artifactResolutionServiceHelp=クライアントのSAMLアーティファクト解決サービス。これは、KeycloakがSOAP ArtifactResolveメッセージを送信するエンドポイントです。このバインディングのURLがない場合は、空白のままにすることができます。 invalidateSecret=無効化 notBeforeSetToNow=クライアントに「以前」を設定 @@ -2941,8 +2941,8 @@ filterByRoles=レルムロールでフィルタリングする strictTransportSecurityHelp=Strict-Transport-Security HTTPヘッダーは、ブラウザーに常にHTTPSを使用するよう指示します。ブラウザーがこのヘッダーを認識すると、max-ageで指定された期間(1年間)の間、サブドメインを含め、サイトへのアクセスはHTTPSのみになります。<1>詳細はこちら loginTimeoutHelp=ユーザーがログインを完了するまでの最大時間。30分以上など、比較的長めに設定することを推奨します。 trusted-hosts.tooltip=信頼され、クライアント登録サービスの呼び出しやクライアントURIの値としての使用が許可されているホストのリストです。ホスト名またはIPアドレスを使用できます。先頭にアスタリスク(*)を付けると(例:「*.example.com」)、ドメインexample.com全体が信頼されます。 -disableTypeClaimCheckHelp=アイデンティティー・プロバイダーから受信したトークンの「typ」クレームの検証を無効にします。「off」の場合、typeクレームは検証されます(デフォルト)。 -passSubjectHelp=ログインフェーズ中に、オプションのlogin_hintクエリー・パラメーターをSAML認証リクエストのSubjectに転送します。 +disableTypeClaimCheckHelp=アイデンティティープロバイダーから受信したトークンの「typ」クレームの検証を無効にします。「off」の場合、typeクレームは検証されます(デフォルト)。 +passSubjectHelp=ログインフェーズ中に、オプションのlogin_hintクエリーパラメーターをSAML認証リクエストのSubjectに転送します。 rsaGenerated=rsa-generated memberofLdapAttributeHelp=「ユーザーロール取得戦略」がGET_GROUPS_FROM_USER_MEMBEROF_ATTRIBUTEの場合にのみ使用されます。LDAPユーザーのLDAP属性名を指定します。この属性には、ユーザーが所属するグループが含まれます。通常はデフォルトの「memberOf」になります。 krbPrincipalAttributeHelp=Kerberosプリンシパルを参照するLDAP属性の名前。KeycloakでKerberos/SPNEGO認証が成功した後、適切なLDAPユーザーを検索するために使用されます。この属性が空の場合、LDAPユーザーはKerberosプリンシパルの最初の部分に対応するLDAPユーザー名に基づいて検索されます。例えば、プリンシパルが「john@KEYCLOAK.ORG」の場合、LDAPユーザー名は「john」であると想定されます。 @@ -2951,39 +2951,39 @@ passwordPoliciesHelp.hashIterations=パスワードを保存または検証す idpUnlinkSuccess=プロバイダーのリンクは削除されました providerType=プロバイダーの種類 disableNonceHelp=認証リクエストでnonceパラメーターを送信しないでください。nonceパラメーターはデフォルトで送信され、検証されます。 -deleteClientProfile=このクライアントプロファイルを削除します。 -emptyClientProfiles=プロファイルがありません。 +deleteClientProfile=このクライアントプロファイルを削除します +emptyClientProfiles=プロファイルがありません clientRegisterPolicyDeleteSuccess=クライアント登録ポリシーが正常に削除されました。 keysFilter.ACTIVE=アクティブな鍵 artifactResolutionServiceUrlHelp=アーティファクトからSAMLアサーションを取得するために使用する必要があるURL(SAMLのArtifactResolve)。 deleteSuccess=属性グループが削除されました。 -clientRolesConditionTooltip=この条件評価中にチェックされるクライアント・ロール。クライアントに、設定で指定されたクライアント・ロールと同じ名前のクライアント・ロールが少なくとも1つある場合、条件はtrueと評価されます。 +clientRolesConditionTooltip=この条件評価中にチェックされるクライアントロール。クライアントに、設定で指定されたクライアントロールと同じ名前のクライアントロールが少なくとも1つある場合、条件はtrueと評価されます。 policyProvider.client=1 つ以上のクライアントのセットがオブジェクトにアクセスできるようにするパーミッションの条件を定義します。 mapperTypeMsadUserAccountControlManager=msad-user-account-control-mapper deleteNodeFail=ノードを削除できませんでした:{{error}} host-sending-registration-request-must-match.label=クライアント登録リクエストを送信するホストが一致する必要があります。 emailInvalid=有効なメールアドレスを入力してください。 -deleteConfirmDialog_one=選択した{{count}}件のユーザーを完全に削除してもよろしいですか? +deleteConfirmDialog_one=選択した{{count}}件のユーザーを完全に削除してもよいですか? rename=名前を変更する profilesConfigType=次の方法で設定します: keyLabel=鍵 -eventTypes.IDENTITY_PROVIDER_RESPONSE_ERROR.name=アイデンティティー・プロバイダー応答エラー +eventTypes.IDENTITY_PROVIDER_RESPONSE_ERROR.name=アイデンティティープロバイダーのレスポンスエラー FAIL=インポート失敗 -noMappersInstructions=現在、このアイデンティティー・プロバイダーのマッパーは存在しません。 +noMappersInstructions=現在、このアイデンティティープロバイダーのマッパーは存在しません。 attributesDropdown=属性ドロップダウン selectMethodType.generate=生成 -syncChangedUsers=変更されたユーザーを同期する -client-roles-condition.tooltip=この条件評価中にチェックされるクライアント・ロール。クライアントに、設定で指定されたクライアント・ロールと同じ名前のクライアント・ロールが少なくとも1つある場合、条件はtrueと評価されます。 +syncChangedUsers=変更されたユーザーの同期 +client-roles-condition.tooltip=この条件の評価中にチェックされるクライアントロール。クライアントに、設定で指定されたクライアントロールと同じ名前のクライアントロールが少なくとも1つある場合、条件はtrueと評価されます。 TERMS_AND_CONDITIONS=利用規約(TERMS_AND_CONDITIONS) -userRoleMappingUpdatedSuccess=ユーザーロールのマッピングが正常に更新されました。 +userRoleMappingUpdatedSuccess=ユーザーロールのマッピングが正常に更新されました memberofLdapAttribute=メンバーLDAP属性 copyOf={{name}}のコピー eventTypes.REMOVE_TOTP.description=TOTPの削除 authenticationExplain=認証は、さまざまなクレデンシャルの種類を設定および管理できる領域です。 dropNonexistingGroupsDuringSync=同期中に存在しないグループを削除する -deletePermissionConfirm=パーミッション{{permission}}を削除してもよろしいですか? +deletePermissionConfirm=パーミッション{{permission}}を削除してもよいですか? clientUpdaterTrustedHosts=信頼できるホスト -excludeIssuerFromAuthenticationResponseHelp=これがオンの場合、OpenID Connect認証レスポンスにパラメーター「iss」が含まれません。クライアントが「iss」パラメーターをサポートしていない古いOIDC / OAuth2アダプターを使用している場合に便利です。 +excludeIssuerFromAuthenticationResponseHelp=オンの場合、OpenID Connect認証レスポンスにパラメーター「iss」が含まれません。クライアントが「iss」パラメーターをサポートしていない古いOIDC / OAuth2アダプターを使用している場合に便利です。 emptyPermissionInstructions=パーミッションを作成する場合は、以下のボタンをクリックして、リソースベースまたはスコープベースのパーミッションを作成してください。 requiredClient=少なくとも1つのクライアントを追加してください。 help=ヘルプ @@ -2992,13 +2992,13 @@ chooseAPolicyProvider=ポリシープロバイダーの選択 searchGroups=グループの検索 httpPostBindingLogoutHelp=HTTP-POSTバインディングを使用してリクエストに応答するかどうかを設定します。オフの場合は、HTTP-REDIRECTバインディングが使用されます。 orderDialogIntro=ログインページまたはアカウントUIに表示されるプロバイダーの順序。行ハンドルをドラッグして順序を変更できます。 -targetClaimHelp=ポリシーが取得するターゲット・クレームを指定します。 +targetClaimHelp=ポリシーが取得する対象のクレームを指定します。 client-attributes-condition.tooltip=この条件の評価中にチェックされるクライアント属性。クライアントが、設定で指定されたクライアント属性と同じ名前と値を持つすべてのクライアント属性を持っている場合、条件はtrueと評価されます。 -isAccessTokenJWT=アクセストークンはJWTです。 +isAccessTokenJWT=アクセストークンはJWTです noSessionsForClient=現在、このクライアントのアクティブなセッションはありません。 enableHelp=ヘルプを有効にする cibaExpiresIn=有効期限 -noAvailableIdentityProviders=利用可能なアイデンティティー・プロバイダーがありません。 +noAvailableIdentityProviders=利用可能なアイデンティティープロバイダーがありません。 searchType.default=デフォルト検索 eventTypes.INVALID_SIGNATURE_ERROR.description=無効な署名エラー includeSubGroups=サブグループのユーザーを含める @@ -3017,34 +3017,34 @@ selectThemeType=テーマタイプの選択 authenticatorRefConfig.maxAge.label=オーセンティケーター参照の最大有効期間 lightweightAccessToken=常に軽量アクセストークンを使用する itemDeleteConfirmTitle=項目を削除しますか? -itemDeletedSuccess=項目が削除されました。 +itemDeletedSuccess=項目が削除されました emptySelection=空の選択 editBtn=編集ボタン acceptBtn=編集ボタンの承認 bruteForceMode.TemporaryLockout=一時的にロックアウト multivalued=マルチバリュー to 属性。サイズと値を適切に検証するには、組み込みのバリデーターのいずれかを使用してください。 -sendClientIdOnLogoutHelp=ログアウト・リクエストで「client_id」パラメーターを送信するかどうか。 +sendClientIdOnLogoutHelp=ログアウトリクエストで「client_id」パラメーターを送信するかどうか。 defaultLanguage=デフォルト translationValue=翻訳値 addTranslationDialogHelperText=デフォルトの言語に基づいた翻訳が必要です。 translationError=保存する前に翻訳を追加してください: {{error}} -senderEnvelopePlaceholder=送信者のエンベロープ・メールアドレス +senderEnvelopePlaceholder=送信者のエンベロープメールアドレス smtpPortPlaceholder=SMTPポート (デフォルトは 25) loginUsernamePlaceholder=ログインユーザー名 -identityBrokeringLink=アイデンティティー・ブローカー・リンク +identityBrokeringLink=アイデンティティーブローカーリンク importFileHelp=鍵をインポートするファイル logo=ロゴ eventTypes.INVITE_ORG.description=組織へのユーザーの招待 eventTypes.INVITE_ORG_ERROR.name=組織へのユーザー招待時のエラー error-invalid-value=「{{0}}」は無効な値です。 loa-condition-level=認証レベル(LoA) -eventTypes.INVITE_ORG.name=組織にユーザーを招待する +eventTypes.INVITE_ORG.name=組織へのユーザーの招待 eventTypes.INVITE_ORG_ERROR.description=組織へのユーザー招待時のエラー missingLastNameMessage=「'{{0}}」: 姓を指定してください。 includeInLightweight.tooltip=軽量アクセストークンにクレームを追加する必要があるか termsAndConditionsUserAttribute=利用規約の承諾タイムスタンプ -supportJwtClaimInIntrospectionResponseHelp=オンの場合、「Accept: application/jwt」ヘッダーを使用するイントロスペクション・リクエストには、JWTのアクセストークンとしてエンコードされたイントロスペクションの結果を含む「jwt」という名前のクレームも含まれます。 +supportJwtClaimInIntrospectionResponseHelp=オンの場合、「Accept: application/jwt」ヘッダーを使用するイントロスペクションリクエストには、JWTのアクセストークンとしてエンコードされたイントロスペクションの結果を含む「jwt」という名前のクレームも含まれます。 chooseBindingType=バインドの種類を選択してください。 kcNumberFormat=数値のフォーマット bruteForceMode.PermanentLockout=永久にロックアウト @@ -3069,15 +3069,15 @@ searchForEffectiveMessageBundles=メッセージバンドルの検索 translationDeleteConfirmDialog=選択した{{count}}件の翻訳を完全に削除してもよいですか? missingPasswordMessage=「{{0}}」: パスワードを指定してください。 resourceFile=リソースファイル -hardcodedRole=ユーザーがプロバイダーからインポートされるときに、そのユーザーに対するロール・マッピングをハードコードします。 +hardcodedRole=ユーザーがプロバイダーからインポートされるときに、そのユーザーに対するロールマッピングをハードコードします。 invalidEmailMessage=「{{0}}」: 無効なメールアドレスです。 error-empty=「{{0}}」の値を指定してください。 transientUserTooltip=このユーザーはKeycloakのデータベースに保存されません。元のアイデンティティープロバイダーから提供されたデータのみから構築されます。 loa-max-age.tooltip=この認証レベルが有効となる最大秒数です。特定の認証レベルが要求され、ユーザーが指定秒数より前に既にこのレベルで認証されている場合、再認証は求められません。ただし、指定秒数より後に認証されている場合は、再度このレベルで認証する必要があります。設定値0は、このレベルが要求されるたびに、ユーザーは必ずこのレベルで再認証を求められます。 cancelBtn=編集ボタンのキャンセル -itemDeleteConfirm=この項目を完全に削除してもよろしいですか? -createItem=項目を作成する -avatarImage=アバターイメージ +itemDeleteConfirm=この項目を完全に削除してもよいですか? +createItem=項目の作成 +avatarImage=アバター画像 tokenExpirationHelp=トークンの有効期限を設定します。有効期限が切れたトークンは定期的にデータベースから削除されます。 fetchRoles=ロールの取得 bruteForceMode.PermanentAfterTemporaryLockout=一時的なロックアウト後に永久にロックアウト @@ -3096,25 +3096,25 @@ deleteMessageBundle=メッセージバンドルの{{key}}を削除します emptyEffectiveMessageBundlesInstructions=上記の検索ボックスでは、テーマ、機能、言語別に、必要な効果的なメッセージを検索できます。 authenticatorRefConfig.value.label=オーセンティケーター参照 referralHelp=LDAPリファラルに従うか無視するかを指定します。リファラルを有効にすると、LDAPサーバーが他のどのLDAPサーバーを使用するか決定するため、認証に時間がかかる可能性があることに注意してください。これには信頼できないサーバーが含まれる可能性があります。 -supportJwtClaimInIntrospectionResponse=イントロスペクション・レスポンスでJWTクレームをサポート +supportJwtClaimInIntrospectionResponse=イントロスペクションレスポンスでJWTクレームをサポート selectBindType=バインドの種類を選択する kcNumberUnFormat=数値のフォーマット解除 -sendIdTokenOnLogoutHelp=ログアウト・リクエストで「id_token_hint」パラメーターを送信するかどうか。 +sendIdTokenOnLogoutHelp=ログアウトリクエストで「id_token_hint」パラメーターを送信するかどうか。 addAttributeDisplayDescriptionTranslation=表示説明の翻訳を追加する noLanguagesSearchResultsInstructions=上の検索バーをクリックして言語を検索してください addTranslationsDialogRowsTable=翻訳ダイアログの行テーブルを追加する parRequestUriLifespan=プッシュされた認可リクエストのリクエストURIの有効期間 -searchClientRegistration=ポリシーを検索する +searchClientRegistration=ポリシーの検索 dynamicScopeFormat=動的スコープ形式 selectAll=すべてを選択 xRobotsTagHelp=ページが検索エンジンに表示されないようにします。<1>詳細 clientRegisterPolicyDeleteError=クライアント登録ポリシーを削除できませんでした:{{error}} deleteAllTranslationsSuccess=翻訳が正常に削除されました。 themeType=テーマタイプ -addTranslation=翻訳を追加する -sendClientIdOnLogout=ログアウト・リクエストで「client_id」を送信する +addTranslation=翻訳の追加 +sendClientIdOnLogout=ログアウトリクエストで「client_id」を送信する replyToEmailPlaceholder=メールアドレスに返信 -client-updater-source-roles.label=エンティティー・ロールの更新 +client-updater-source-roles.label=エンティティーロールの更新 generatedUserInfoHelp=ユーザー情報エンドポイントによって提供されるユーザー情報の例を参照してください。 verifyEmailHelpText=最初のログイン後、またはアドレスの変更を送信した後、ユーザーに電子メール アドレスの確認を求めます。 referrerPolicy=リファラーポリシー @@ -3125,8 +3125,8 @@ realmOverrides=レルムの上書き noRealmOverridesSearchResultsInstructions=上の検索バーをクリックして、レルムの上書きを検索します。 referral=参照 customValue=カスタム値 -realmOverridesDescription=レルム・オーバーライドを使用すると、レルム全体に適用される翻訳を指定できます。これらの翻訳は、テーマで指定された翻訳をオーバーライドします。 -itemDelete=項目を削除する +realmOverridesDescription=レルムのオーバーライドを使用すると、レルム全体に適用される翻訳を指定できます。これらの翻訳は、テーマで指定された翻訳をオーバーライドします。 +itemDelete=項目の削除 noItems=項目がありません noItemsInstructions=このレルムにはまだ項目が作成されていません。開始するには項目を作成してください。 writableSelection=書き込み可能な選択 @@ -3140,8 +3140,8 @@ associatedPolicy=関連するポリシー notBeforeNowClear=クライアントに対して「以前」をクリアしました。 startBySearchingAUser=ユーザーの検索から始める error-invalid-length-too-short=「{{0}}」は最小の長さは{{1}}でなければなりません。 -searchItem=項目を検索する -lightweightAccessTokenHelp=オンの場合、軽量アクセストークンが常に使用されます。オフの場合、デフォルトでは使用されませんが、クライアント・ポリシー・エグゼキューターを使用して有効化することは可能です。 +searchItem=項目の検索 +lightweightAccessTokenHelp=オンの場合、軽量アクセストークンが常に使用されます。オフの場合、デフォルトでは使用されませんが、クライアントポリシーエグゼキューターを使用して有効化することは可能です。 fetchRolesHelp=デフォルトでは、認可リクエストで送信されたトークンに含まれるロールのみが、ユーザーにロールが付与されているかどうかの確認に使用されます。この設定を有効にすると、ポリシーはトークン内のロールを無視し、代わりにユーザーに関連付けられたロールを確認します。 ownerHelp=このリソースの所有者。 parRequestUriLifespanHelp=リクエストURIの有効期間を表す数値。デフォルト値は1分です。 @@ -3153,13 +3153,13 @@ hasWords=語句を含む loa-condition-level.tooltip=認証レベル。この値は常に0以上の整数である必要があります。認証フロー内のサブフローは、常に最下位レベルから上位レベルの順に並べる必要があります。 selectMapperType=マッパーの種類を選択する unsyncedSelection=同期されていない選択 -resourceDetailsTypeHelp=このリソースのタイプ。同じタイプの異なるリソース・インスタンスをグループ化するために使用できます。 +resourceDetailsTypeHelp=このリソースのタイプ。同じタイプの異なるリソースインスタンスをグループ化するために使用できます。 addAttributeTranslation=「{{fieldName}}」フィールドの翻訳を追加する translationKey=キー -sendIdTokenOnLogout=ログアウト・リクエストで「id_token_hint」を送信する +sendIdTokenOnLogout=ログアウトリクエストで「id_token_hint」を送信する deleteAllTranslationsError=翻訳の削除中にエラーが発生しました: {{error}} -multivaluedHelp=この属性が複数の値をサポートする場合に設定します。この設定はインジケーターであり、検証を有効にするものではありません。 -resetAction=リセット・アクション +multivaluedHelp=この属性がマルチバリューをサポートする場合に設定します。この設定はインジケーターであり、検証を有効にするものではありません。 +resetAction=リセットアクション dynamicScopeFormatHelp=これは、システムがスコープ名と変数を抽出するために使用する正規表現です。 removeAnnotationText=アノテーションの削除 updatePermissionSuccess=パーミッションの更新に成功しました @@ -3168,25 +3168,25 @@ error-invalid-date=「{{0}}」は無効な日付です。 error-invalid-length=「{{0}}」の長さは{{1}}から{{2}}まででなければなりません。 error-invalid-number=「{{0}}」は無効な数値です。 loa-max-age=最大時間 -effectiveMessageBundlesDescription=効果的なメッセージ・バンドルとは、特定の言語、テーマ、テーマタイプに対応する翻訳のセットです。また、レルムのオーバーライドも考慮され、オーバーライドが優先されます。 -itemDeleteError=項目を削除できませんでした:{{error}} -selectClientAssertionSigningAlg=クライアント・アサーション署名アルゴリズムを選択する +effectiveMessageBundlesDescription=効果的なメッセージバンドルとは、特定の言語、テーマ、テーマタイプに対応する翻訳のセットです。また、レルムのオーバーライドも考慮されます(オーバーライドが優先されます)。 +itemDeleteError=項目を削除できませんでした: {{error}} +selectClientAssertionSigningAlg=クライアントアサーション署名アルゴリズムを選択する searchClientAuthorizationPermission=パーミッションを検索する -userNotSaved=ユーザーは保存されていません:{{error}} -addAttributeTranslationBtn=翻訳ボタンを追加する +userNotSaved=ユーザーは保存されていません: {{error}} +addAttributeTranslationBtn=翻訳ボタンの追加 addAttributeTranslationInfo=「{{fieldName}}」フィールドの横にあるアイコンを使用して、このフィールドの翻訳を追加します。 addTranslationsModalTitle=翻訳を追加する searchForLanguage=言語を検索する theme=テーマ itemSaveSuccessful=保存に成功しました。 editTranslationValue=翻訳値を編集する -selectRequestObjectRequired=必要なリクエスト・オブジェクトを選択する -selectAuthorizationEncryptedResponseAlg=認可暗号化レスポンス・アルゴリズムを選択する -selectAuthorizationEncryptedResponseEnc=認可暗号化レスポンス・エンコーディングを選択する -selectUserInfoSignedResponseAlgorithm=ユーザー情報署名レスポンス・アルゴリズムを選択する -generatedCodeTextArea=生成されたコード・テキストエリア -selectEmailTheme=メールテーマを選択する -selectEvictionMinute=キャッシュ追い出し時刻(分)を選択する +selectRequestObjectRequired=必要なリクエストオブジェクトの選択 +selectAuthorizationEncryptedResponseAlg=認可暗号化レスポンスアルゴリズムの選択 +selectAuthorizationEncryptedResponseEnc=認可暗号化レスポンスエンコーディングの選択 +selectUserInfoSignedResponseAlgorithm=ユーザー情報署名レスポンスアルゴリズムを選択する +generatedCodeTextArea=生成されたコードテキストエリア +selectEmailTheme=メールテーマの選択 +selectEvictionMinute=キャッシュ追い出し時刻(分)の選択 organizationAliasHelp=エイリアスは、主に組織内部で参照することを目的とした形式を用いて、組織を一意に識別します。例えば、組織関連のクレームをトークンに含める場合や、カスタムテーマ内で使用する場合などです。 organizationUsersLeftError=組織からユーザーを削除できませんでした: {{error}} organizationUsersLeft_one=ユーザーが組織から脱退しました @@ -3194,7 +3194,7 @@ noIdentityProvider=このレルムにはアイデンティティープロバイ adminPermissionsEnabled=管理者パーミッション verifiableCredentialsEnabled=検証可能なクレデンシャル adminPermissionsEnabledHelp=有効にすると、レルム内の管理者パーミッションを管理できるようになります。 -selectAuthorizationSignedResponseAlgorithm=認可署名付きレスポンス・アルゴリズムを選択する +selectAuthorizationSignedResponseAlgorithm=認可署名付きレスポンスアルゴリズムの選択 emptyOrganizations=組織がありません memberList=メンバー一覧 removeMember=メンバーの削除 @@ -3208,31 +3208,31 @@ organizationRedirectUrlHelp=登録完了後、または組織への招待を承 caseSensitiveOriginalUsernameHelp=有効にすると、ユーザーを連携する際に、アイデンティティープロバイダーから取得した元のユーザー名がそのまま保持されます。無効にすると、アイデンティティープロバイダーから取得したユーザー名は小文字に変換されるため、大文字と小文字が区別される元のユーザー名と一致しない可能性があります。この設定は、サーバー上のユーザー名は常に小文字であるため、連携されたアイデンティティーに関連付けられたユーザー名にのみ影響します。 organizationSaveError=組織を保存できませんでした: {{error}} noIdentityProviderInstructions=このレルムにはまだアイデンティティープロバイダーがありません。この組織にアイデンティティープロバイダーをリンクしたい場合は、左側のナビゲーションバーの「アイデンティティープロバイダー」のセクションに移動してアイデンティティープロバイダーを作成してください。 -selectEvictionHour=キャッシュ追い出し時刻(時間)を選択する +selectEvictionHour=キャッシュ追い出し時刻(時間)の選択 emptyIdentityProviderLink=この組織のアイデンティティープロバイダーがありません organizationDetails=組織の詳細 realmSelector=レルムセレクター selectIdTokenEncryptionKeyManagementAlgorithm=IDトークン暗号化鍵管理アルゴリズムを選択する inviteSentError=招待状を送信できませんでした: {{error}} -selectEditMode=編集モードを選択する +selectEditMode=編集モードの選択 organizationUsersAdded_other={{count}}人のユーザーが組織に追加されました inviteSent=招待状が送信されました。 selectIdTokenEncryptionContentEncryptionAlgorithm=IDトークン暗号化コンテンツ暗号化アルゴリズムを選択する -selectRequestObjectEncryption=リクエスト・オブジェクトの暗号化を選択する -selectAuthScopes=認可スコープを選択する -selectUnmanagedAttributePolicy=管理されていない属性ポリシーを選択する -selectEvictionDay=キャッシュ追い出し日を選択する +selectRequestObjectEncryption=リクエストオブジェクトの暗号化の選択 +selectAuthScopes=認可スコープの選択 +selectUnmanagedAttributePolicy=管理されていない属性ポリシーの選択 +selectEvictionDay=キャッシュ追い出し日の選択 verifiableCredentialsEnabledHelp=有効にすると、このレルムで検証可能なクレデンシャルを管理できるようになります。 organizationsExplain=組織とメンバーを管理します。 organizationsEnabledHelp=有効にすると、組織の管理が可能になります。無効にすると、既存の組織は維持されますが、管理やメンバーの認証はできなくなります。 selectUserInfoResponseEncryptionContentEncryptionAlgorithm=ユーザー情報レスポンス暗号化コンテンツ暗号化アルゴリズムを選択する selectUserInfoResponseEncryptionKeyManagementAlgorithm=ユーザー情報レスポンス暗号化鍵管理アルゴリズムを選択する -selectRequestObjectEncoding=リクエスト・オブジェクトのエンコーディングを選択する +selectRequestObjectEncoding=リクエストオブジェクトのエンコーディングの選択 organizationUsersLeft_other={{count}}件のユーザーが組織から脱退しました emptyMembers=メンバーがいません disableConfirmOrganizationTitle=組織を無効にしますか? addRealmUser=レルムユーザーの追加 -selectRequestObjectSignatureAlgorithm=リクエスト・オブジェクトの署名アルゴリズムを選択する +selectRequestObjectSignatureAlgorithm=リクエストオブジェクトの署名アルゴリズムを選択する selectAccessTokenSignatureAlgorithm=アクセストークンの署名アルゴリズムを選択する orgainzatinoDeleteError=クライアントを削除できません: {{error}} redirectUrl=リダイレクトURL @@ -3250,12 +3250,12 @@ organizationRemoveError=組織からユーザーを削除できませんでし eventTypes.UPDATE_CREDENTIAL.name=クレデンシャルの更新 adminPermissionName=パーミッション名 noAssignedPoliciesInstructions=このパーミッションには割り当てられたポリシーがありません。 -emptyAssignExistingPolicies=既存のポリシーはありません。 +emptyAssignExistingPolicies=既存のポリシーはありません redirectWhenEmailMatches=メールドメインが一致したらリダイレクトする userAddedOrganization_other={{count}}個の組織がユーザーに追加されました duplicateGroupWarning=多数のサブグループを持つグループの複製はサポートされていません。複製するグループに多数のサブグループが含まれていないことを確認してください。 removeInvalidUsers=検索中に無効なユーザーを削除する -couldNotFetchClientRoleMappings=クライアント・ロールのマッピングを取得できませんでした:{{error}} +couldNotFetchClientRoleMappings=クライアントロールのマッピングを取得できませんでした: {{error}} enforceAccessToHelpText=パーミッションが適用されるリソースを指定します。 viewAll=すべて表示 titlePermissions=パーミッション @@ -3265,7 +3265,7 @@ createPermissionOfType=このパーミッションは{{resourceType}}に適用 resourceScopeHelpText=リソースのスコープを指定します。これは、パーミッションが付与されるリソースの種類を決定するために使用されます。 allClients=すべてのクライアント assignedPolicies=割り当てられたポリシー -assignExistingPolicies=既存のポリシーを割り当てる +assignExistingPolicies=既存のポリシーの割り当て policy=ポリシー policyTypeHelpText=ポリシーの種類を指定します。これは、パーミッションが付与されるポリシーの種類を決定するために使用されます。 authorizationScopeDetailsTitle=認可スコープの詳細 @@ -3277,8 +3277,8 @@ linkUpdatedSuccessful=アイデンティティープロバイダーのリンク idpAccountEmailVerificationHelp=IdPアカウントのメール検証の独立したタイムアウト executeActionsHelp=アクション実行の独立したタイムアウト eventTypes.REMOVE_CREDENTIAL.name=クレデンシャルの削除 -groupDuplicated=グループが複製されました。 -duplicateAGroup=グループを複製する +groupDuplicated=グループが複製されました +duplicateAGroup=グループの複製 permissionUsersHelpText=このパーミッションによって許可されるユーザーを指定します。 requiredPolicies=最低1つのポリシーを追加してください。 permissionNameHelpText=パーミッションの名前。この名前は管理コンソールでパーミッションを識別するために使用されます。 @@ -3288,9 +3288,9 @@ unlinkError=組織からアイデンティティープロバイダーのリン eventTypes.REMOVE_CREDENTIAL.description=クレデンシャルの削除 eventTypes.REMOVE_CREDENTIAL_ERROR.name=クレデンシャルの削除エラー eventTypes.REMOVE_CREDENTIAL_ERROR.description=クレデンシャルの削除エラー -darkModeEnabledHelp=有効にすると、オペレーティング・システムの設定(ライトモードまたはダークモード)またはユーザー・エージェントの設定に基づいて、テーマのダークバージョンが適用されます。無効にすると、ライトバージョンのみが使用されます。この設定は、ダークバージョンとライトバージョンの両方をサポートするテーマにのみ適用されます。テーマがこの機能をサポートしていない場合、この設定は効果がありません。 +darkModeEnabledHelp=有効にすると、オペレーティングシステムの設定(ライトモードまたはダークモード)またはユーザーエージェントの設定に基づいて、テーマのダークバージョンが適用されます。無効にすると、ライトバージョンのみが使用されます。この設定は、ダークバージョンとライトバージョンの両方をサポートするテーマにのみ適用されます。テーマがこの機能をサポートしていない場合、この設定は効果がありません。 noGroupMembershipsText=このユーザーはどのグループにも属していません。 -noGroupMemberships=メンバーシップがありません。 +noGroupMemberships=メンバーシップがありません somethingWentWrongDescription=残念ながら、予期しないエラーが発生しました。 specificClients=特定のクライアント specificResourceType=特定の{{resourceType}} @@ -3301,18 +3301,18 @@ currentRealm=現在のレルム currentRealmExplain=このレルムが選択されています。 forgotPasswordHelp=パスワードを忘れた場合の独立したタイムアウト removeConfirmOrganizationTitle=組織を削除しますか? -disableConfirmUser=このユーザーを無効にしてもよろしいですか? -somethingWentWrong=何らかの問題が発生しました。 +disableConfirmUser=このユーザーを無効にしてもよいですか? +somethingWentWrong=何らかの問題が発生しました policyType=ポリシータイプ enforceAccessTo=アクセスを強制する authorizationScopeDetailsSubtitle=認可スコープは、リソースに対して実行できるアクションを定義します。 -showMemberships=メンバーシップを表示する +showMemberships=メンバーシップの表示 childGroupEvents=子グループイベント emptyUserOrganizationsInstructions=まだ組織がありません。組織に参加するか、組織への招待を送信してください。 joinOrganization=組織への参加 -tryAgain=再試行してください。 +tryAgain=再試行してください eventTypes.UPDATE_CREDENTIAL_ERROR.name=クレデンシャルの更新エラー -createNewPolicy=新しいポリシーを作成する +createNewPolicy=新しいポリシーの作成 linkUpdateError=アイデンティティープロバイダーのリンクを更新できませんでした: {{error}} linkedOrganization=リンクされた組織 organization=組織 @@ -3321,26 +3321,26 @@ emailVerificationHelp=メール検証の独立したタイムアウト sendInvitation=招待状の送信 sendInvite=招待状の送信 organizationRemoveConfirm_one=選択した組織からユーザーを削除してもよいですか? -termsAndConditionsDeclined=続行するには利用規約に同意する必要があります。 +termsAndConditionsDeclined=続行するには利用規約に同意する必要があります signatureAlgorithmIdentityProviderMetadata=署名アルゴリズムSAML IdPメタデータ connectionTrace=接続トレース -signatureAlgorithmIdentityProviderMetadataHelp=SAMLアイデンティティー・プロバイダーのメタデータに使用する署名アルゴリズム。署名アルゴリズムがない場合、メタデータは署名されません。 +signatureAlgorithmIdentityProviderMetadataHelp=SAMLアイデンティティープロバイダーのメタデータに使用する署名アルゴリズム。署名アルゴリズムがない場合、メタデータは署名されません。 resourceScope=リソーススコープ allResourceType=すべての{{resourceType}} -membershipEvents=メンバーシップ・イベント +membershipEvents=メンバーシップイベント unAssignPolicy=割り当て解除 authorizationScopeDetailsName=名前 assignedPolicyType.user=ユーザー connectionTraceHelp=有効にすると、受信および送信のLDAP ASN.1 BERパケットがエラー出力ストリームにダンプされます。このオプションを本番環境で有効にする場合は、LDAPサーバーとの間で送受信されるすべてのデータが公開されるため、注意してください。 organizationName=組織名 userAddedOrganizationError=ユーザーに組織を追加できませんでした: {{error}} -showMembershipsTitle={{username}}グループ・メンバーシップ +showMembershipsTitle={{username}}グループメンバーシップ createPermissionPolicy=ポリシーを作成する emptyPermissionPoliciesInstructions=このレルムにはポリシーは存在しません。 noPermissionSearchResultsInstructions=フィルターに一致するパーミッションはありませんでした。 organizationRemovedSuccess=ユーザーが組織から削除されました noResultsFound=結果が見つかりません -permissionsSubTitle=パーミッションは、1 つのタイプのリソースまたは複数のリソースへのアクセスを制御します。 +permissionsSubTitle=パーミッションは、1つのリソースまたは1種類の複数のリソースへのアクセスを制御します。 organizationRemoveConfirm_other={{count}}件の選択した組織からユーザーを削除してもよいですか? targetAudience=対象のオーディエンス grantedScope=付与されるスコープは次の通りです。 @@ -3360,12 +3360,12 @@ deniedScope=拒否されたスコープは次の通りです。 authTokenUrl=認証トークンURL authTokenScopeHelp=スコープはスペースで区切られ、トークン収集時にscopeパラメーターとして使用されます(例:「basic sendmail」) authTokenClientIdHelp=トークン収集中に使用されるclient_id(例: mykeycloak-sendmail-client) -chooseAResourceType=リソースの種類を選択してください。 +chooseAResourceType=リソースの種類を選択してください tokenTokenUrlHelp=トークンを収集するためのトークンエンドポイント: Keycloakの例: http://localhost/auth/realms/my-realm/protocol/openid-connect/token enableDebugSMTP=SMTPのデバッグの有効化 authScopeSelectHelp=サーバーからパーミッションを照会するために使用する認可スコープを選択します。 permissionEvaluationAlertTitle=選択されたユーザーには選択されたリソースへのアクセス権がありません -uploadGeneratedThemeJar=生成されたテーマのJARファイルをアップロードする +uploadGeneratedThemeJar=生成されたテーマのJARファイルのアップロード signatureMaxExpHelp=JWTの最大有効期限。トークンは認証直前に生成する必要があります。この期間を過ぎると、古すぎるため無効とみなされます。未定義の場合、デフォルト値は60秒です。 groupsResources=グループ resourceTypeHelpText=このパーミッションで許可する{{resourceType}}指定します。空白のままにすると、すべて選択したのと同じになります。 @@ -3378,7 +3378,7 @@ deleteConfirmUsers_other={{count}}件のユーザーを削除しますか? deleteConfirmUsers_one=ユーザー{{name}}を削除しますか? themeColorInfo=ここで、PatternFlyのカラー変数を設定し、「テーマ jar」ファイルを作成できます。このファイルをダウンロードしてprovidersフォルダーに配置し、テーマをレルムに適用できます。 UNMANAGED=管理されていない -downloadThemeJar=テーマのJARファイルをダウンロードする +downloadThemeJar=テーマのJARファイルのダウンロード syncUsersStarted=ユーザーの同期が開始されました。 deleteConfirmRealm_one=レルム{{name}}を削除しますか? deleteConfirmRealm_other={{count}}個のレルムを削除しますか? @@ -3397,14 +3397,14 @@ resourceType.Clients=このレルム内のクライアントに対して実行 resourceType.Organizations=このレルム内の組織に対して実行できる操作へのアクセスを制御します。 resourceType.Roles=このレルムのロールに対して実行できる操作へのアクセスを制御します。 resourceType.Users=このレルムのユーザーに対して実行できる操作へのアクセスを制御します。 -removeInvalidUsersHelp=検索実行時にユーザー・ストレージからユーザーを利用できない場合、ローカル・データベースからユーザーを削除します。これがtrueの場合、対応するユーザー・ストレージから利用できなくなったユーザーは、ユーザー検索時にローカル・データベースから削除されます。falseの場合、以前ユーザー・ストレージからインポートされたユーザーは、そのユーザーがユーザー・ストレージから利用できなくなっても、読み取り専用かつ無効化された状態でローカル・データベースに保持されます。例えば、ユーザーがLDAPから直接削除された場合や、「ユーザーDN」が無効である場合などです。この動作は、ユーザーがまだキャッシュされていない場合にのみ発生することに注意してください。 +removeInvalidUsersHelp=検索実行時にユーザーストレージからユーザーを利用できない場合、ローカルデータベースからユーザーを削除します。これがtrueの場合、対応するユーザーストレージから利用できなくなったユーザーは、ユーザー検索時にローカルデータベースから削除されます。falseの場合、以前ユーザーストレージからインポートされたユーザーは、そのユーザーがユーザーストレージから利用できなくなっても、読み取り専用かつ無効化された状態でローカルデータベースに保持されます。例えば、ユーザーがLDAPから直接削除された場合や、「ユーザーDN」が無効である場合などです。この動作は、ユーザーがまだキャッシュされていない場合にのみ発生することに注意してください。 targetAudienceHelp=対象のオーディエンスを設定します。これは、トークンエンドポイントの付与リクエストで「audience」パラメーターを使用する場合と同じです。「audience」パラメーターは、現時点ではトークン交換グラントでのみ使用できます。他のグラントをシミュレートする場合、または「audience」パラメーターなしでトークン交換グラントをシミュレートする場合は、このパラメータをー空のままにしておくことをお勧めします。 authorizationScope.Roles.map-role-client-scope=このロールをクライアントのクライアントスコープに適用します。 authorizationScope.Roles.map-role=このロールをユーザーまたはグループにマッピングします。 authorizationScope.Roles.map-role-composite=このロールを複合ロールとして別のロールに適用します。 usersResources=ユーザー clientsResources=クライアント -resourceType.IdentityProviders=このレルム内のアイデンティティー・プロバイダーに対して実行できる操作へのアクセスを制御します。 +resourceType.IdentityProviders=このレルム内のアイデンティティープロバイダーに対して実行できる操作へのアクセスを制御します。 authorizationScope.Groups.view=このグループの表示 permissionsEvaluationInstructions=特定のリソースに対するアクセス権があるかどうかを確認するには、ユーザーを選択してください。下の「評価」ボタンをクリックすると、右側のパネルに結果が表示されます。 permissionEvaluationPreview=パーミッション評価のプレビュー @@ -3431,12 +3431,12 @@ authorizationScope.Groups.manage-members=グループメンバーの管理 authorizationScope.Groups.manage-membership=グループメンバーの追加または削除 shownOnLoginPage=ログインページに表示されます linkSuccessful=アイデンティティープロバイダーが組織に正常にリンクされました -errorSavingTranslations=翻訳の保存中にエラーが発生しました:{{error}} +errorSavingTranslations=翻訳の保存中にエラーが発生しました: {{error}} searchProvider=プロバイダーの検索 crlCache=CRLキャッシュ userVerify.discouraged=非推奨 selectIdentityProvider=アイデンティティープロバイダーを選択してください -expandRow=行を展開する +expandRow=行の展開 addOrganizationId.label=組織IDの追加 temporaryService=一時的な管理者サービスアカウント。できるだけ早く、永続的な管理者サービスアカウントに置き換えてください。 addOrganizationAttributes.label=組織属性の追加 @@ -3448,7 +3448,7 @@ sentInvitation=招待状の送信 emptyIdentityProviderLinkInstructions=この組織にはまだアイデンティティープロバイダーがありません。この組織にアイデンティティープロバイダーをリンクしてください。 disableConfirmUserTitle=ユーザーを無効にしますか? clearRealmCacheHelp=これにより、すべてのレルムのエントリーがクリアされます。 -clearKeysCacheHelp=外部公開鍵のキャッシュからすべてのエントリーをクリアします。これらは外部クライアントまたはアイデンティティー・プロバイダーの鍵です。これにより、すべてのレルムのすべてのエントリーがクリアされます。 +clearKeysCacheHelp=外部公開鍵のキャッシュからすべてのエントリーをクリアします。これらは外部クライアントまたはアイデンティティープロバイダーの鍵です。これにより、すべてのレルムのすべてのエントリーがクリアされます。 addOrganizationAttributes.help=有効にすると、トークンにマップされた各組織で組織属性が使用できるようになります。 eventTypes.AUTHREQID_TO_TOKEN.description=認可リクエストIDとトークンの交換 passSubject=サブジェクトの受け渡し @@ -3457,18 +3457,18 @@ addOrganizationId.help=有効にすると、トークンにマップされた組 identityProviderUnlink=アイデンティティープロバイダーのリンクを解除しますか? shownOnLoginPageHelp=チェックすると、このアイデンティティープロバイダーがログイン ページに表示されます。 linkError=アイデンティティープロバイダーを組織にリンクできませんでした: {{error}} -membershipType=メンバーシップ・タイプ -clearCacheSuccess=キャッシュが正常にクリアされました。 -MANAGED=管理された +membershipType=メンバーシップタイプ +clearCacheSuccess=キャッシュが正常にクリアされました +MANAGED=管理されている clearCrlCacheHelp=CRLキャッシュからすべてのエントリーをクリアします。CRLキャッシュは、証明書失効リスト(CRL)が有効な場合にX.509オーセンティケーターの性能を向上させます。このアクションは、すべてのレルムのCRLエントリーをすべてクリアします。 -clearCacheError=キャッシュをクリアできませんでした:{{error}} +clearCacheError=キャッシュをクリアできませんでした: {{error}} eventTypes.AUTHREQID_TO_TOKEN.name=認可リクエストIDとトークンの交換 selectIdTokenSignatureAlgorithm=IDトークン署名アルゴリズムを選択する userInvitedOrganization_other=ユーザーへの{{count}}件の招待状が送信されました。 userInvitedOrganizationError=ユーザーを組織に招待できませんでした: {{error}} unLinkSuccessful=アイデンティティープロバイダーのリンクが解除されました -filterByMembershipType=メンバーシップの種類でフィルタリングする -organizationsMembersListError=組織のメンバーを取得できませんでした:{{error}} +filterByMembershipType=メンバーシップタイプでのフィルタリング +organizationsMembersListError=組織のメンバーを取得できませんでした: {{error}} identityProviderUnlinkConfirm=このアイデンティティープロバイダーのリンクを解除してもよろしいですか? editGroup=グループを編集する deprecated=非推奨 diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ka.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ka.properties index f33856423288..7e3330c28747 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ka.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ka.properties @@ -41,9 +41,9 @@ active=აქტიური profile=პროფილი executorName=სახელი credentials=ავტორიზაციის დეტალები -mapperTypeLdapAttributeMapper=ჩაშენებული-ldap-ის-ატრიბუტის-ამსახველი +mapperTypeLdapAttributeMapper=hardcoded-ldap-attribute-mapper selectMethodType.import=შემოტანა -mapperTypeHardcodedLdapGroupMapper=ჩაშენებული-ldap-ის-ჯგუფის-ამსახველი +mapperTypeHardcodedLdapGroupMapper=hardcoded-ldap-group-mapper added=დამატებულია test=შემოწმება valueLabel=მნიშვნელობა @@ -70,7 +70,7 @@ username=მომხმარებლის სახელი eventTypes.IMPERSONATE.name=განსახიერება claims=მოთხოვნები representation=რეპრეზენტაცია -cibaBackhannelTokenDeliveryModes.poll=პოლი +cibaBackhannelTokenDeliveryModes.poll=გამოკითხვა eventTypes.IMPERSONATE.description=განსახიერება refresh=განახლება continue=გაგრძელება @@ -1237,7 +1237,7 @@ eventTypes.UPDATE_TOTP_ERROR.name=TOTP-ის განახლების შ resetPasswordConfirmation=ახალი პაროლის დადასტურება htmlDisplayName=HTML საჩვენებელი სახელი newClientProfileName=კლიენტის პროფილის სახელი -generateNewKeys=ახალი გასაღებების გენერაცია +generateNewKeys=RSA გასაღებების გენერაცია attributeFriendlyName=ატრიბუტი [მეგობრული სახელი] createPolicy=კლიენტის პოლიტიკის შექმნა failureFactor=მაქს. შესვლის ჩავარდნები @@ -1342,3 +1342,8 @@ clientProfile=კლიენტის პროფილის დეტალ keyForCodeExchange=PKCE მეთოდი cpu=CPU processorCount=პროცესორების როდენობა +deprecated=მოძველებულია +passSubject=სუბიექტის გადაცემა +webAuthnPolicyPasskeysEnabled=საკვანძო გასაღებების ჩართვა +validity=სერტიფიკატის ვადა +forceArtifactBinding=არტეფაქტების ნაძალადევი მიბმა diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties index 7337c0198d45..631a9bc58b30 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_pt_BR.properties @@ -331,7 +331,7 @@ reevaluate=Re-avaliar continue=Continuar details=Detalhes selectScope=Selecione um escopo -generateNewKeys=Gerar novas chaves +generateNewKeys=Gerar chaves RSA offlineSessionIdle=Sessão Offline Inativa periodicChangedUsersSync=Sincronização periódica de usuários alterados searchScope=Escopo de pesquisa @@ -1025,3 +1025,59 @@ realmInfo=Informações do realm readBlog=Leia o blog joinCommunity=Participar da comunidade loggedInAsTempAdminUser=Você está conectado como um usuário administrador temporário. Para reforçar a segurança, crie uma conta de administrador permanente e exclua a temporária. +updateFirstLogin=Atualizar primeiro login +flowUsedBy=Uso deste fluxo +updateExecutorSuccess=Executor atualizado com sucesso +ldapAttributeHelp=Nome do atributo mapeado no objeto LDAP. Por exemplo: 'cn', 'sn', 'mail', 'street', etc. +claimFilterNameHelp=Nome do claim essencial +revocationDescription=Esta é uma forma de revogar todas as sessões ativas e tokens de acesso. "Não antes de" significa que você pode revogar qualquer token emitido antes da data especificada. +eventTypes.CODE_TO_TOKEN_ERROR.description=Erro de código para token +eventTypes.CODE_TO_TOKEN_ERROR.name=Erro de código para token +termsOfServiceUrl=URL dos termos de serviço +requestObject.request_uri\ only=Apenas URI de request +addressClaim.street.label=Nome do Atributo de Usuário para Logradouro +applyToResourceTypeFlag=Aplicar ao tipo de recurso +testError=Erro ao tentar conectar no LDAP: '{{error}}' +groupObjectClassesHelp=Classe (ou classes) do objeto de grupo. Separe com vírgulas se forem necessárias várias classes. Em uma implantação LDAP típica, pode ser 'groupOfNames'. No Active Directory, geralmente é 'group'. +filterByClients=Filtrar por clientes +createPolicyOfType=Criar política {{policyType}} +flowUsedByDescription=Este fluxo é usado pelo seguinte {{value}} +createClientScope=Criar escopo de cliente +expireTimeHelp=Define o horário após o qual a política NÃO DEVE ser concedida. Só será concedida se a data/hora atual for anterior ou igual a esse valor. +noRolesInstructions-roles=Você não criou nenhum papel neste realm. Crie um papel para começar. +editIdPMapper=Editar Mapeador de Provedor de Identidade +unmanagedAttributesHelpText=Atributos não gerenciados são atributos de usuário que não estão explicitamente definidos na configuração do perfil de usuário. Por padrão, os atributos não gerenciados estão como `Desativado` e não ficam disponíveis em nenhum contexto, como registro, conta e console de administração. Ao definir como `Ativado`, os atributos não gerenciados são totalmente reconhecidos pelo servidor e acessíveis em todos os contextos, útil ao migrar um realm existente para o perfil de usuário declarativo sem ter ainda todos os atributos definidos na configuração. Ao definir como `Somente administradores podem escrever`, os atributos não gerenciados podem ser gerenciados apenas pelo console de administração e API, útil quando há atributos personalizados que usuários podem alterar, mas deseja-se restringir novos atributos apenas aos administradores. Ao definir como `Somente administradores podem visualizar`, os atributos não gerenciados são somente leitura e acessíveis apenas pelo console de administração e API. +unmanagedAttributePolicy.ADMIN_VIEW=Somente administradores podem visualizar +unmanagedAttributePolicy.ADMIN_EDIT=Somente administradores podem escrever +confirmPasswordDoesNotMatch=Senha e confirmação não correspondem. +eventTypes.DELETE_ACCOUNT_ERROR.description=Erro ao excluir conta +passwordPoliciesHelp.length=O número mínimo de caracteres exigidos para a senha. +removeImportedUsersSuccess=Usuários importados foram removidos. +eventTypes.VERIFY_PROFILE_ERROR.name=Erro ao verificar perfil +authnContextClassRefsHelp=Lista ordenada de AuthnContext ClassRefs solicitados. +useLowerCaseBearerType=Usar o tipo bearer em letras minúsculas nas respostas de token +useRfc9068AccessTokenType=Usa "at+jwt" como tipo de cabeçalho de token de acesso +ldapAttributeNameHelp=Nome do atributo LDAP que será adicionado ao novo usuário durante o registro. +createAGroup=Criar um grupo +effectiveProtocolMappersHelp=Contém todos os escopos de cliente padrão e os escopos opcionais selecionados. Todos os mapeadores de protocolo e mapeamentos de escopo de papéis desses escopos de cliente serão utilizados ao gerar o token de acesso emitido para seu cliente. +exportSuccess=Realm exportado com sucesso. +scopePermissions.groups.manage-description=Políticas que decidem se um administrador pode gerenciar esse grupo +testClusterFail=Falha ao verificar a disponibilidade de: {{failedNodes}}. Corrija ou desregistre os nós com falha no cluster e tente novamente +queryExtensions=Consultar Extensões Suportadas +signingKeysConfig=Configuração das chaves de assinatura +validateBindDn=Você precisa informar o DN do administrador do LDAP +addedGroupMembership=Associação a grupo adicionada +resourceDeletedSuccess=O recurso excluído com sucesso +useRefreshTokensHelp=Se esta opção estiver ativada, um refresh_token será criado e adicionado à resposta do token. Se estiver desativada, nenhum refresh_token será gerado. +getStarted=Para começar, selecione um provedor da lista abaixo. +signedJWTConfirm=Gere uma chave privada e um certificado para o cliente na aba Chaves. +searchAdminEventsBtn=Pesquisar eventos administrativos +deleteDialogDescription=Tem certeza de que deseja excluir permanentemente o grupo de atributos <1>{{group}}? +importResourceSuccess=O recurso foi importado com sucesso +deleteNodeBody=Tem certeza de que deseja excluir permanentemente o nó "{{node}}" +usersAdded_one={{count}} usuário(s) adicionado(s) ao grupo +resourcesAndScopes=Recursos e Escopos +eventTypes.UPDATE_CONSENT_ERROR.description=Erro ao atualizar consentimento +eventTypes.UPDATE_CONSENT_ERROR.name=Erro ao atualizar consentimento +overrideActionTokensHelp=Substitui as configurações padrão de tempo máximo antes que uma ação permitida enviada por um usuário (como um e-mail de recuperação de senha) expire, para uma ação específica. Este valor é recomendado ser curto, pois espera-se que o usuário aja rapidamente a uma ação criada por ele mesmo. +searchByName=Pesquisar por nome diff --git a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties index f73949a8bfa2..eda9d942d12e 100644 --- a/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties +++ b/js/apps/admin-ui/maven-resources-community/theme/keycloak.v2/admin/messages/messages_ru.properties @@ -3494,3 +3494,4 @@ cibaBackchannelTokenDeliveryModeHelp=Указывает, как CD (Consumption deprecated=Устаревший cibaBackchannelTokenDeliveryMode=Способ доставки токенов через back-channel to атрибут. Для этого обязательно используйте любой из встроенных валидаторов для правильной проверки размера и значений. +tokenIntrospectionUrl=URL интроспекции токена diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 8ff9d7e24907..0c4056691dd8 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -431,6 +431,7 @@ guiOrder=Display Order friendlyName=Friendly name of attribute to search for in assertion. You can leave this blank and specify a name instead. testSuccess=Successfully connected to LDAP userInfoUrl=User Info URL +tokenIntrospectionUrl=Token Introspection URL displayOnConsentScreen=Display on consent screen noClientPolicies=No client policies defaultAdminInitiatedActionLifespanHelp=Maximum time before an action permit sent to a user by administrator is expired. This value is recommended to be long to allow administrators to send e-mails for users that are currently offline. The default timeout can be overridden immediately before issuing the token. diff --git a/js/apps/admin-ui/package.json b/js/apps/admin-ui/package.json index 2fa937fc74c1..35b5da7703dd 100644 --- a/js/apps/admin-ui/package.json +++ b/js/apps/admin-ui/package.json @@ -91,7 +91,7 @@ "admin-ui": "file:", "file-saver": "^2.0.5", "flat": "^6.0.1", - "i18next": "^25.2.1", + "i18next": "^25.3.1", "i18next-fetch-backend": "^6.0.0", "jszip": "^3.10.1", "keycloak-js": "^26.2.0", @@ -99,7 +99,7 @@ "p-debounce": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "^7.59.0", + "react-hook-form": "^7.60.0", "react-i18next": "^15.5.3", "react-router-dom": "^6.30.1", "reactflow": "^11.11.4", @@ -107,11 +107,11 @@ }, "devDependencies": { "@axe-core/playwright": "^4.10.2", - "@playwright/test": "^1.53.1", + "@playwright/test": "^1.53.2", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@types/dagre": "^0.7.52", + "@types/dagre": "^0.7.53", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", "@types/react": "^18.3.18", @@ -123,8 +123,8 @@ "properties-file": "^3.5.12", "ts-node": "^10.9.2", "uuid": "^11.1.0", - "vite": "^7.0.0", - "vite-plugin-checker": "^0.9.3", + "vite": "^7.0.3", + "vite-plugin-checker": "^0.10.0", "vite-plugin-dts": "^4.5.4", "vitest": "^3.2.4" } diff --git a/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx b/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx index 53da3c825cf7..7198033b408f 100644 --- a/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx +++ b/js/apps/admin-ui/src/clients/keys/GenerateKeyDialog.tsx @@ -93,25 +93,29 @@ export const KeyForm = ({ {format !== CERT_PEM && ( )} - - + {!useFile && ( + <> + + + + )} ); }; diff --git a/js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx b/js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx index 3a4146d442d8..c90975d1ffc8 100644 --- a/js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/DiscoverySettings.tsx @@ -71,6 +71,12 @@ const Fields = ({ readOnly, isOIDC }: DiscoverySettingsProps) => { required: isOIDC ? "" : t("required"), }} /> + {isOIDC && ( { await assertInvalidUrlNotification(page, "token"); await clickRevertButton(page); + await setUrl(page, "tokenIntrospection", "invalid"); + await clickSaveButton(page); + await assertInvalidUrlNotification(page, "tokenIntrospection"); + await clickRevertButton(page); + await assertJwksUrlExists(page); - await switchOff(page, "#config\\.useJwksUrl"); + await page.getByText("Use JWKS URL").click(); await assertJwksUrlExists(page, false); await assertPkceMethodExists(page, false); diff --git a/js/apps/keycloak-server/package.json b/js/apps/keycloak-server/package.json index 8f5a84df9679..0c81cd5453f9 100644 --- a/js/apps/keycloak-server/package.json +++ b/js/apps/keycloak-server/package.json @@ -12,6 +12,6 @@ "@types/gunzip-maybe": "^1.4.2", "@types/tar-fs": "^2.0.4", "gunzip-maybe": "^1.4.2", - "tar-fs": "^3.0.10" + "tar-fs": "^3.1.0" } } diff --git a/js/libs/keycloak-admin-client/package.json b/js/libs/keycloak-admin-client/package.json index 328ad6690434..495af867a747 100644 --- a/js/libs/keycloak-admin-client/package.json +++ b/js/libs/keycloak-admin-client/package.json @@ -44,11 +44,11 @@ "url-template": "^3.1.1" }, "devDependencies": { - "@faker-js/faker": "^9.8.0", + "@faker-js/faker": "^9.9.0", "@types/chai": "^5.2.2", "@types/lodash-es": "^4.17.12", "@types/mocha": "^10.0.10", - "@types/node": "^24.0.4", + "@types/node": "^24.0.10", "chai": "^5.2.0", "lodash-es": "^4.17.21", "mocha": "^11.7.1", diff --git a/js/libs/ui-shared/package.json b/js/libs/ui-shared/package.json index c67f55282da8..c8e9b692606a 100644 --- a/js/libs/ui-shared/package.json +++ b/js/libs/ui-shared/package.json @@ -48,12 +48,12 @@ "@patternfly/react-icons": "^5.4.2", "@patternfly/react-styles": "^5.4.1", "@patternfly/react-table": "^5.4.16", - "i18next": "^25.2.1", + "i18next": "^25.3.1", "keycloak-js": "^26.2.0", "lodash-es": "^4.17.21", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-hook-form": "7.59.0", + "react-hook-form": "7.60.0", "react-i18next": "^15.5.3" }, "devDependencies": { @@ -62,8 +62,8 @@ "@types/react-dom": "^18.3.5", "@vitejs/plugin-react-swc": "^3.10.2", "rollup-plugin-peer-deps-external": "^2.2.4", - "vite": "^7.0.0", - "vite-plugin-checker": "^0.9.3", + "vite": "^7.0.3", + "vite-plugin-checker": "^0.10.0", "vite-plugin-dts": "^4.5.4", "vite-plugin-lib-inject-css": "^2.2.2", "vitest": "^3.2.4" diff --git a/js/package.json b/js/package.json index e1ec619a02bc..82355eef8d3e 100644 --- a/js/package.json +++ b/js/package.json @@ -21,9 +21,9 @@ "devDependencies": { "@eslint/compat": "^1.3.1", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.30.0", - "@types/node": "^24.0.4", - "eslint": "^9.29.0", + "@eslint/js": "^9.30.1", + "@types/node": "^24.0.10", + "eslint": "^9.30.1", "eslint-config-prettier": "^10.1.5", "eslint-plugin-lodash": "^8.0.0", "eslint-plugin-playwright": "^2.2.0", @@ -36,7 +36,7 @@ "prettier": "^3.6.2", "tslib": "^2.8.1", "typescript": "^5.8.3", - "typescript-eslint": "^8.35.0", + "typescript-eslint": "^8.36.0", "wireit": "^0.14.12" }, "pnpm": { diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 016711ad77a4..9afbe550ad77 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -10,40 +10,40 @@ importers: devDependencies: '@eslint/compat': specifier: ^1.3.1 - version: 1.3.1(eslint@9.29.0) + version: 1.3.1(eslint@9.30.1) '@eslint/eslintrc': specifier: ^3.3.1 version: 3.3.1 '@eslint/js': - specifier: ^9.30.0 - version: 9.30.0 + specifier: ^9.30.1 + version: 9.30.1 '@types/node': - specifier: ^24.0.4 - version: 24.0.4 + specifier: ^24.0.10 + version: 24.0.10 eslint: - specifier: ^9.29.0 - version: 9.29.0 + specifier: ^9.30.1 + version: 9.30.1 eslint-config-prettier: specifier: ^10.1.5 - version: 10.1.5(eslint@9.29.0) + version: 10.1.5(eslint@9.30.1) eslint-plugin-lodash: specifier: ^8.0.0 - version: 8.0.0(eslint@9.29.0) + version: 8.0.0(eslint@9.30.1) eslint-plugin-playwright: specifier: ^2.2.0 - version: 2.2.0(eslint@9.29.0) + version: 2.2.0(eslint@9.30.1) eslint-plugin-prettier: specifier: ^5.5.1 - version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.6.2) + version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1))(eslint@9.30.1)(prettier@3.6.2) eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.29.0) + version: 7.37.5(eslint@9.30.1) eslint-plugin-react-compiler: specifier: 19.0.0-beta-714736e-20250131 - version: 19.0.0-beta-714736e-20250131(eslint@9.29.0) + version: 19.0.0-beta-714736e-20250131(eslint@9.30.1) eslint-plugin-react-hooks: specifier: ~5.2.0 - version: 5.2.0(eslint@9.29.0) + version: 5.2.0(eslint@9.30.1) husky: specifier: ^9.1.7 version: 9.1.7 @@ -60,8 +60,8 @@ importers: specifier: ^5.8.3 version: 5.8.3 typescript-eslint: - specifier: ^8.35.0 - version: 8.35.0(eslint@9.29.0)(typescript@5.8.3) + specifier: ^8.36.0 + version: 8.36.0(eslint@9.30.1)(typescript@5.8.3) wireit: specifier: ^0.14.12 version: 0.14.12 @@ -84,8 +84,8 @@ importers: specifier: ^5.4.16 version: 5.4.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) i18next: - specifier: ^25.2.1 - version: 25.2.1(typescript@5.8.3) + specifier: ^25.3.1 + version: 25.3.1(typescript@5.8.3) i18next-fetch-backend: specifier: ^6.0.0 version: 6.0.0 @@ -102,11 +102,11 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-hook-form: - specifier: ^7.59.0 - version: 7.59.0(react@18.3.1) + specifier: ^7.60.0 + version: 7.60.0(react@18.3.1) react-i18next: specifier: ^15.5.3 - version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 15.5.3(i18next@25.3.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) react-router-dom: specifier: ^6.30.1 version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -115,8 +115,8 @@ importers: specifier: workspace:* version: link:../../libs/keycloak-admin-client '@playwright/test': - specifier: ^1.53.1 - version: 1.53.1 + specifier: ^1.53.2 + version: 1.53.2 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -128,7 +128,7 @@ importers: version: 18.3.5(@types/react@18.3.18) '@vitejs/plugin-react-swc': specifier: ^3.10.2 - version: 3.10.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 3.10.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -136,14 +136,14 @@ importers: specifier: ^1.30.1 version: 1.30.1 vite: - specifier: ^7.0.0 - version: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + specifier: ^7.0.3 + version: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) vite-plugin-checker: - specifier: ^0.9.3 - version: 0.9.3(eslint@9.29.0)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + specifier: ^0.10.0 + version: 0.10.0(eslint@9.30.1)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@24.0.4)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 4.5.4(@types/node@24.0.10)(rollup@4.44.2)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) apps/admin-ui: dependencies: @@ -184,8 +184,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 i18next: - specifier: ^25.2.1 - version: 25.2.1(typescript@5.8.3) + specifier: ^25.3.1 + version: 25.3.1(typescript@5.8.3) i18next-fetch-backend: specifier: ^6.0.0 version: 6.0.0 @@ -208,11 +208,11 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-hook-form: - specifier: ^7.59.0 - version: 7.59.0(react@18.3.1) + specifier: ^7.60.0 + version: 7.60.0(react@18.3.1) react-i18next: specifier: ^15.5.3 - version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 15.5.3(i18next@25.3.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) react-router-dom: specifier: ^6.30.1 version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -225,10 +225,10 @@ importers: devDependencies: '@axe-core/playwright': specifier: ^4.10.2 - version: 4.10.2(playwright-core@1.53.1) + version: 4.10.2(playwright-core@1.53.2) '@playwright/test': - specifier: ^1.53.1 - version: 1.53.1 + specifier: ^1.53.2 + version: 1.53.2 '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -239,8 +239,8 @@ importers: specifier: ^16.3.0 version: 16.3.0(@testing-library/dom@10.4.0)(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/dagre': - specifier: ^0.7.52 - version: 0.7.52 + specifier: ^0.7.53 + version: 0.7.53 '@types/file-saver': specifier: ^2.0.7 version: 2.0.7 @@ -255,7 +255,7 @@ importers: version: 18.3.5(@types/react@18.3.18) '@vitejs/plugin-react-swc': specifier: ^3.10.2 - version: 3.10.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 3.10.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) cross-env: specifier: ^7.0.3 version: 7.0.3 @@ -270,22 +270,22 @@ importers: version: 3.5.12 ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.12.0)(@types/node@24.0.4)(typescript@5.8.3) + version: 10.9.2(@swc/core@1.12.0)(@types/node@24.0.10)(typescript@5.8.3) uuid: specifier: ^11.1.0 version: 11.1.0 vite: - specifier: ^7.0.0 - version: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + specifier: ^7.0.3 + version: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) vite-plugin-checker: - specifier: ^0.9.3 - version: 0.9.3(eslint@9.29.0)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + specifier: ^0.10.0 + version: 0.10.0(eslint@9.30.1)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@24.0.4)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 4.5.4(@types/node@24.0.10)(rollup@4.44.2)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.0.4)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + version: 3.2.4(@types/node@24.0.10)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) apps/create-keycloak-theme: dependencies: @@ -320,8 +320,8 @@ importers: specifier: ^1.4.2 version: 1.4.2 tar-fs: - specifier: ^3.0.10 - version: 3.0.10 + specifier: ^3.1.0 + version: 3.1.0 libs/keycloak-admin-client: dependencies: @@ -336,8 +336,8 @@ importers: version: 3.1.1 devDependencies: '@faker-js/faker': - specifier: ^9.8.0 - version: 9.8.0 + specifier: ^9.9.0 + version: 9.9.0 '@types/chai': specifier: ^5.2.2 version: 5.2.2 @@ -348,8 +348,8 @@ importers: specifier: ^10.0.10 version: 10.0.10 '@types/node': - specifier: ^24.0.4 - version: 24.0.4 + specifier: ^24.0.10 + version: 24.0.10 chai: specifier: ^5.2.0 version: 5.2.0 @@ -361,7 +361,7 @@ importers: version: 11.7.1 ts-node: specifier: ^10.9.2 - version: 10.9.2(@swc/core@1.12.0)(@types/node@24.0.4)(typescript@5.8.3) + version: 10.9.2(@swc/core@1.12.0)(@types/node@24.0.10)(typescript@5.8.3) libs/ui-shared: dependencies: @@ -381,8 +381,8 @@ importers: specifier: ^5.4.16 version: 5.4.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) i18next: - specifier: ^25.2.1 - version: 25.2.1(typescript@5.8.3) + specifier: ^25.3.1 + version: 25.3.1(typescript@5.8.3) keycloak-js: specifier: ^26.2.0 version: 26.2.0 @@ -396,11 +396,11 @@ importers: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) react-hook-form: - specifier: 7.59.0 - version: 7.59.0(react@18.3.1) + specifier: 7.60.0 + version: 7.60.0(react@18.3.1) react-i18next: specifier: ^15.5.3 - version: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 15.5.3(i18next@25.3.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) devDependencies: '@types/lodash-es': specifier: ^4.17.12 @@ -413,25 +413,25 @@ importers: version: 18.3.5(@types/react@18.3.18) '@vitejs/plugin-react-swc': specifier: ^3.10.2 - version: 3.10.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 3.10.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) rollup-plugin-peer-deps-external: specifier: ^2.2.4 - version: 2.2.4(rollup@4.44.1) + version: 2.2.4(rollup@4.44.2) vite: - specifier: ^7.0.0 - version: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + specifier: ^7.0.3 + version: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) vite-plugin-checker: - specifier: ^0.9.3 - version: 0.9.3(eslint@9.29.0)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + specifier: ^0.10.0 + version: 0.10.0(eslint@9.30.1)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@24.0.4)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 4.5.4(@types/node@24.0.10)(rollup@4.44.2)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vite-plugin-lib-inject-css: specifier: ^2.2.2 - version: 2.2.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + version: 2.2.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@24.0.4)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + version: 3.2.4(@types/node@24.0.10)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) themes-vendor: dependencies: @@ -462,19 +462,19 @@ importers: devDependencies: '@rollup/plugin-commonjs': specifier: ^28.0.6 - version: 28.0.6(rollup@4.44.1) + version: 28.0.6(rollup@4.44.2) '@rollup/plugin-node-resolve': specifier: ^16.0.1 - version: 16.0.1(rollup@4.44.1) + version: 16.0.1(rollup@4.44.2) '@rollup/plugin-replace': specifier: ^6.0.2 - version: 6.0.2(rollup@4.44.1) + version: 6.0.2(rollup@4.44.2) '@rollup/plugin-terser': specifier: ^0.4.4 - version: 0.4.4(rollup@4.44.1) + version: 0.4.4(rollup@4.44.2) rollup: - specifier: ^4.44.1 - version: 4.44.1 + specifier: ^4.44.2 + version: 4.44.2 packages: @@ -666,10 +666,6 @@ packages: resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==} engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.1': - resolution: {integrity: sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==} - engines: {node: '>=6.9.0'} - '@babel/runtime@7.27.6': resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} engines: {node: '>=6.9.0'} @@ -729,152 +725,158 @@ packages: resolution: {integrity: sha512-mepCf/e9+SKYy1d02/UkvSy6+6MoyXhVxP8lLDfA7BPE1X1d4dR0sZznmbM8/XVJ1GPM+Svnx7Xj6ZweByWUkw==} engines: {node: '>17.0.0'} - '@esbuild/aix-ppc64@0.25.5': - resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + '@esbuild/aix-ppc64@0.25.6': + resolution: {integrity: sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.5': - resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + '@esbuild/android-arm64@0.25.6': + resolution: {integrity: sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.5': - resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + '@esbuild/android-arm@0.25.6': + resolution: {integrity: sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.5': - resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + '@esbuild/android-x64@0.25.6': + resolution: {integrity: sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.5': - resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + '@esbuild/darwin-arm64@0.25.6': + resolution: {integrity: sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.5': - resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + '@esbuild/darwin-x64@0.25.6': + resolution: {integrity: sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.5': - resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + '@esbuild/freebsd-arm64@0.25.6': + resolution: {integrity: sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.5': - resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + '@esbuild/freebsd-x64@0.25.6': + resolution: {integrity: sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.5': - resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + '@esbuild/linux-arm64@0.25.6': + resolution: {integrity: sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.5': - resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + '@esbuild/linux-arm@0.25.6': + resolution: {integrity: sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.5': - resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + '@esbuild/linux-ia32@0.25.6': + resolution: {integrity: sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.5': - resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + '@esbuild/linux-loong64@0.25.6': + resolution: {integrity: sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.5': - resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + '@esbuild/linux-mips64el@0.25.6': + resolution: {integrity: sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.5': - resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + '@esbuild/linux-ppc64@0.25.6': + resolution: {integrity: sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.5': - resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + '@esbuild/linux-riscv64@0.25.6': + resolution: {integrity: sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.5': - resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + '@esbuild/linux-s390x@0.25.6': + resolution: {integrity: sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.5': - resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + '@esbuild/linux-x64@0.25.6': + resolution: {integrity: sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.5': - resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + '@esbuild/netbsd-arm64@0.25.6': + resolution: {integrity: sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.5': - resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + '@esbuild/netbsd-x64@0.25.6': + resolution: {integrity: sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.5': - resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + '@esbuild/openbsd-arm64@0.25.6': + resolution: {integrity: sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.5': - resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + '@esbuild/openbsd-x64@0.25.6': + resolution: {integrity: sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.5': - resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + '@esbuild/openharmony-arm64@0.25.6': + resolution: {integrity: sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.6': + resolution: {integrity: sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.5': - resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + '@esbuild/win32-arm64@0.25.6': + resolution: {integrity: sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.5': - resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + '@esbuild/win32-ia32@0.25.6': + resolution: {integrity: sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.5': - resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + '@esbuild/win32-x64@0.25.6': + resolution: {integrity: sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -898,44 +900,40 @@ packages: eslint: optional: true - '@eslint/config-array@0.20.1': - resolution: {integrity: sha512-OL0RJzC/CBzli0DrrR31qzj6d6i6Mm3HByuhflhl4LOBiWxN+3i6/t/ZQQNii4tjksXi8r2CRW1wMpWA2ULUEw==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.2.3': - resolution: {integrity: sha512-u180qk2Um1le4yf0ruXH3PYFeEZeYC3p/4wCTKrr2U1CmGdzGi3KtY0nuPDH48UJxlKCC5RDzbcbh4X0XlqgHg==} + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.14.0': resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.15.0': - resolution: {integrity: sha512-b7ePw78tEWWkpgZCDYkbqDOP8dmM6qe+AOC6iuJqlq1R/0ahMAeH3qynpnqKFGkMltrp44ohV4ubGyvLX28tzw==} + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.29.0': - resolution: {integrity: sha512-3PIF4cBw/y+1u2EazflInpV+lYsSG0aByVIQzAgb1m1MhHFSbqTyNqtBKHgWf/9Ykud+DhILS9EGkmekVhbKoQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.30.0': - resolution: {integrity: sha512-Wzw3wQwPvc9sHM+NjakWTcPx11mbZyiYHuwWa/QfZ7cIRX7WK54PSk7bdyXDaoaopUcMatv1zaQvOAAO8hCdww==} + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.3.2': - resolution: {integrity: sha512-4SaFZCNfJqvk/kenHpI8xvN42DMaoycy4PzKc5otHxRswww1kAt82OlBuwRVLofCACCTZEcla2Ydxv8scMXaTg==} + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@faker-js/faker@9.8.0': - resolution: {integrity: sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==} + '@faker-js/faker@9.9.0': + resolution: {integrity: sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==} engines: {node: '>=18.0.0', npm: '>=9.0.0'} '@humanfs/core@0.19.1': @@ -1114,8 +1112,8 @@ packages: resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.53.1': - resolution: {integrity: sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==} + '@playwright/test@1.53.2': + resolution: {integrity: sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==} engines: {node: '>=18'} hasBin: true @@ -1216,203 +1214,103 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.44.0': - resolution: {integrity: sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==} + '@rollup/rollup-android-arm-eabi@4.44.2': + resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm-eabi@4.44.1': - resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.44.0': - resolution: {integrity: sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==} + '@rollup/rollup-android-arm64@4.44.2': + resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} cpu: [arm64] os: [android] - '@rollup/rollup-android-arm64@4.44.1': - resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.44.0': - resolution: {integrity: sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-arm64@4.44.1': - resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} + '@rollup/rollup-darwin-arm64@4.44.2': + resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.44.0': - resolution: {integrity: sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==} + '@rollup/rollup-darwin-x64@4.44.2': + resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} cpu: [x64] os: [darwin] - '@rollup/rollup-darwin-x64@4.44.1': - resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.44.0': - resolution: {integrity: sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==} + '@rollup/rollup-freebsd-arm64@4.44.2': + resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-arm64@4.44.1': - resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.44.0': - resolution: {integrity: sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.44.1': - resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} + '@rollup/rollup-freebsd-x64@4.44.2': + resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.44.0': - resolution: {integrity: sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': - resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.44.0': - resolution: {integrity: sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.44.1': - resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.44.0': - resolution: {integrity: sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.44.1': - resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} + '@rollup/rollup-linux-arm64-gnu@4.44.2': + resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.44.0': - resolution: {integrity: sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==} + '@rollup/rollup-linux-arm64-musl@4.44.2': + resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.44.1': - resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.44.0': - resolution: {integrity: sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==} + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': - resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': - resolution: {integrity: sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': - resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.44.0': - resolution: {integrity: sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==} + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.44.1': - resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + '@rollup/rollup-linux-riscv64-musl@4.44.2': + resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.44.0': - resolution: {integrity: sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.44.1': - resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.44.0': - resolution: {integrity: sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.44.1': - resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} + '@rollup/rollup-linux-s390x-gnu@4.44.2': + resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.44.0': - resolution: {integrity: sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==} + '@rollup/rollup-linux-x64-gnu@4.44.2': + resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.44.1': - resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} + '@rollup/rollup-linux-x64-musl@4.44.2': + resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.44.0': - resolution: {integrity: sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.44.1': - resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.44.0': - resolution: {integrity: sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-arm64-msvc@4.44.1': - resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} + '@rollup/rollup-win32-arm64-msvc@4.44.2': + resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.44.0': - resolution: {integrity: sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==} + '@rollup/rollup-win32-ia32-msvc@4.44.2': + resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.44.1': - resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.44.0': - resolution: {integrity: sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.44.1': - resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} + '@rollup/rollup-win32-x64-msvc@4.44.2': + resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} cpu: [x64] os: [win32] @@ -1650,8 +1548,8 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} - '@types/dagre@0.7.52': - resolution: {integrity: sha512-XKJdy+OClLk3hketHi9Qg6gTfe1F3y+UFnHxKA2rn9Dw+oXa4Gb378Ztz9HlMgZKSxpPmn4BNVh9wgkpvrK1uw==} + '@types/dagre@0.7.53': + resolution: {integrity: sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ==} '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1695,8 +1593,8 @@ packages: '@types/node@22.13.5': resolution: {integrity: sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==} - '@types/node@24.0.4': - resolution: {integrity: sha512-ulyqAkrhnuNq9pB76DRBTkcS6YsmDALy6Ua63V8OhrOBgbcYt6IOdzpw5P1+dyRIyMerzLkeYWBeOXPpA9GMAA==} + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} '@types/prismjs@1.26.5': resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==} @@ -1727,63 +1625,63 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@typescript-eslint/eslint-plugin@8.35.0': - resolution: {integrity: sha512-ijItUYaiWuce0N1SoSMrEd0b6b6lYkYt99pqCPfybd+HKVXtEvYhICfLdwp42MhiI5mp0oq7PKEL+g1cNiz/Eg==} + '@typescript-eslint/eslint-plugin@8.36.0': + resolution: {integrity: sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.35.0 + '@typescript-eslint/parser': ^8.36.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.35.0': - resolution: {integrity: sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==} + '@typescript-eslint/parser@8.36.0': + resolution: {integrity: sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.35.0': - resolution: {integrity: sha512-41xatqRwWZuhUMF/aZm2fcUsOFKNcG28xqRSS6ZVr9BVJtGExosLAm5A1OxTjRMagx8nJqva+P5zNIGt8RIgbQ==} + '@typescript-eslint/project-service@8.36.0': + resolution: {integrity: sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.35.0': - resolution: {integrity: sha512-+AgL5+mcoLxl1vGjwNfiWq5fLDZM1TmTPYs2UkyHfFhgERxBbqHlNjRzhThJqz+ktBqTChRYY6zwbMwy0591AA==} + '@typescript-eslint/scope-manager@8.36.0': + resolution: {integrity: sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.35.0': - resolution: {integrity: sha512-04k/7247kZzFraweuEirmvUj+W3bJLI9fX6fbo1Qm2YykuBvEhRTPl8tcxlYO8kZZW+HIXfkZNoasVb8EV4jpA==} + '@typescript-eslint/tsconfig-utils@8.36.0': + resolution: {integrity: sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@8.35.0': - resolution: {integrity: sha512-ceNNttjfmSEoM9PW87bWLDEIaLAyR+E6BoYJQ5PfaDau37UGca9Nyq3lBk8Bw2ad0AKvYabz6wxc7DMTO2jnNA==} + '@typescript-eslint/type-utils@8.36.0': + resolution: {integrity: sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.35.0': - resolution: {integrity: sha512-0mYH3emanku0vHw2aRLNGqe7EXh9WHEhi7kZzscrMDf6IIRUQ5Jk4wp1QrledE/36KtdZrVfKnE32eZCf/vaVQ==} + '@typescript-eslint/types@8.36.0': + resolution: {integrity: sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.35.0': - resolution: {integrity: sha512-F+BhnaBemgu1Qf8oHrxyw14wq6vbL8xwWKKMwTMwYIRmFFY/1n/9T/jpbobZL8vp7QyEUcC6xGrnAO4ua8Kp7w==} + '@typescript-eslint/typescript-estree@8.36.0': + resolution: {integrity: sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.35.0': - resolution: {integrity: sha512-nqoMu7WWM7ki5tPgLVsmPM8CkqtoPUG6xXGeefM5t4x3XumOEKMoUZPdi+7F+/EotukN4R9OWdmDxN80fqoZeg==} + '@typescript-eslint/utils@8.36.0': + resolution: {integrity: sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.35.0': - resolution: {integrity: sha512-zTh2+1Y8ZpmeQaQVIc/ZZxsx8UzgKJyNg1PTvjzC7WMhPSVS8bfDX34k1SrwOf016qd5RU3az2UxUNue3IfQ5g==} + '@typescript-eslint/visitor-keys@8.36.0': + resolution: {integrity: sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@uiw/react-textarea-code-editor@3.1.1': @@ -2064,6 +1962,9 @@ packages: brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} @@ -2396,9 +2297,6 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} - end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -2449,8 +2347,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.5: - resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + esbuild@0.25.6: + resolution: {integrity: sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==} engines: {node: '>=18'} hasBin: true @@ -2520,16 +2418,12 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-visitor-keys@4.2.1: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.29.0: - resolution: {integrity: sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==} + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2848,8 +2742,8 @@ packages: resolution: {integrity: sha512-kVqnydqLVMZfVlOuP2nf71cREydlxEKLH43jUXAFdOku/GF+6b9fBg31anoos5XncdhdtiYgL9fheqMrtXRwng==} engines: {node: '>=18'} - i18next@25.2.1: - resolution: {integrity: sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==} + i18next@25.3.1: + resolution: {integrity: sha512-S4CPAx8LfMOnURnnJa8jFWvur+UX/LWcl6+61p9VV7SK2m0445JeBJ6tLD0D5SR0H29G4PYfWkEhivKG5p4RDg==} peerDependencies: typescript: ^5 peerDependenciesMeta: @@ -3504,13 +3398,13 @@ packages: pkg-types@2.1.0: resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} - playwright-core@1.53.1: - resolution: {integrity: sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==} + playwright-core@1.53.2: + resolution: {integrity: sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==} engines: {node: '>=18'} hasBin: true - playwright@1.53.1: - resolution: {integrity: sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==} + playwright@1.53.2: + resolution: {integrity: sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==} engines: {node: '>=18'} hasBin: true @@ -3590,8 +3484,8 @@ packages: peerDependencies: react: '>= 16.8 || 18.0.0' - react-hook-form@7.59.0: - resolution: {integrity: sha512-kmkek2/8grqarTJExFNjy+RXDIP8yM+QTl3QL6m6Q8b2bih4ltmiXxH7T9n+yXNK477xPh5yZT/6vD8sYGzJTA==} + react-hook-form@7.60.0: + resolution: {integrity: sha512-SBrYOvMbDB7cV8ZfNpaiLcgjH/a1c7aK0lK+aNigpf4xWLO8q+o4tcvVurv3c4EOyzn/3dCsYt4GKD42VvJ/+A==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 @@ -3729,13 +3623,8 @@ packages: peerDependencies: rollup: '*' - rollup@4.44.0: - resolution: {integrity: sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rollup@4.44.1: - resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} + rollup@4.44.2: + resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -3964,8 +3853,8 @@ packages: tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} - tar-fs@3.0.10: - resolution: {integrity: sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==} + tar-fs@3.1.0: + resolution: {integrity: sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==} tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} @@ -3990,10 +3879,6 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: - resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -4087,8 +3972,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.35.0: - resolution: {integrity: sha512-uEnz70b7kBz6eg/j0Czy6K5NivaYopgxRjsnAJ2Fx5oTLo3wefTHIbL7AkQr1+7tJCRVpTs/wiM8JR/11Loq9A==} + typescript-eslint@8.36.0: + resolution: {integrity: sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -4201,8 +4086,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite-plugin-checker@0.9.3: - resolution: {integrity: sha512-Tf7QBjeBtG7q11zG0lvoF38/2AVUzzhMNu+Wk+mcsJ00Rk/FpJ4rmUviVJpzWkagbU13cGXvKpt7CMiqtxVTbQ==} + vite-plugin-checker@0.10.0: + resolution: {integrity: sha512-EcAi4M5mzayH386Hc1xWi+vnfl4a+1vrDP9PVEQImUR6tIjItNK6R/98YNnJkaAq1ond2qkA6f+H49aprUgzGA==} engines: {node: '>=14.16'} peerDependencies: '@biomejs/biome': '>=1.7' @@ -4214,7 +4099,7 @@ packages: vite: '>=2.0.0' vls: '*' vti: '*' - vue-tsc: ~2.2.10 + vue-tsc: ~2.2.10 || ^3.0.0 peerDependenciesMeta: '@biomejs/biome': optional: true @@ -4249,8 +4134,8 @@ packages: peerDependencies: vite: '*' - vite@7.0.0: - resolution: {integrity: sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==} + vite@7.0.3: + resolution: {integrity: sha512-y2L5oJZF7bj4c0jgGYgBNSdIu+5HF+m68rn2cQXFbGoShdhV1phX9rbnxy9YXj82aS8MMsCLAAFkRxZeWdldrQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -4565,10 +4450,10 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.36.2 '@ast-grep/napi-win32-x64-msvc': 0.36.2 - '@axe-core/playwright@4.10.2(playwright-core@1.53.1)': + '@axe-core/playwright@4.10.2(playwright-core@1.53.2)': dependencies: axe-core: 4.10.3 - playwright-core: 1.53.1 + playwright-core: 1.53.2 '@babel/code-frame@7.26.2': dependencies: @@ -4721,8 +4606,6 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@babel/runtime@7.27.1': {} - '@babel/runtime@7.27.6': {} '@babel/template@7.26.9': @@ -4783,93 +4666,96 @@ snapshots: '@dagrejs/graphlib@2.2.4': {} - '@esbuild/aix-ppc64@0.25.5': + '@esbuild/aix-ppc64@0.25.6': optional: true - '@esbuild/android-arm64@0.25.5': + '@esbuild/android-arm64@0.25.6': optional: true - '@esbuild/android-arm@0.25.5': + '@esbuild/android-arm@0.25.6': optional: true - '@esbuild/android-x64@0.25.5': + '@esbuild/android-x64@0.25.6': optional: true - '@esbuild/darwin-arm64@0.25.5': + '@esbuild/darwin-arm64@0.25.6': optional: true - '@esbuild/darwin-x64@0.25.5': + '@esbuild/darwin-x64@0.25.6': optional: true - '@esbuild/freebsd-arm64@0.25.5': + '@esbuild/freebsd-arm64@0.25.6': optional: true - '@esbuild/freebsd-x64@0.25.5': + '@esbuild/freebsd-x64@0.25.6': optional: true - '@esbuild/linux-arm64@0.25.5': + '@esbuild/linux-arm64@0.25.6': optional: true - '@esbuild/linux-arm@0.25.5': + '@esbuild/linux-arm@0.25.6': optional: true - '@esbuild/linux-ia32@0.25.5': + '@esbuild/linux-ia32@0.25.6': optional: true - '@esbuild/linux-loong64@0.25.5': + '@esbuild/linux-loong64@0.25.6': optional: true - '@esbuild/linux-mips64el@0.25.5': + '@esbuild/linux-mips64el@0.25.6': optional: true - '@esbuild/linux-ppc64@0.25.5': + '@esbuild/linux-ppc64@0.25.6': optional: true - '@esbuild/linux-riscv64@0.25.5': + '@esbuild/linux-riscv64@0.25.6': optional: true - '@esbuild/linux-s390x@0.25.5': + '@esbuild/linux-s390x@0.25.6': optional: true - '@esbuild/linux-x64@0.25.5': + '@esbuild/linux-x64@0.25.6': optional: true - '@esbuild/netbsd-arm64@0.25.5': + '@esbuild/netbsd-arm64@0.25.6': optional: true - '@esbuild/netbsd-x64@0.25.5': + '@esbuild/netbsd-x64@0.25.6': optional: true - '@esbuild/openbsd-arm64@0.25.5': + '@esbuild/openbsd-arm64@0.25.6': optional: true - '@esbuild/openbsd-x64@0.25.5': + '@esbuild/openbsd-x64@0.25.6': optional: true - '@esbuild/sunos-x64@0.25.5': + '@esbuild/openharmony-arm64@0.25.6': optional: true - '@esbuild/win32-arm64@0.25.5': + '@esbuild/sunos-x64@0.25.6': optional: true - '@esbuild/win32-ia32@0.25.5': + '@esbuild/win32-arm64@0.25.6': optional: true - '@esbuild/win32-x64@0.25.5': + '@esbuild/win32-ia32@0.25.6': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.29.0)': + '@esbuild/win32-x64@0.25.6': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1)': dependencies: - eslint: 9.29.0 + eslint: 9.30.1 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/compat@1.3.1(eslint@9.29.0)': + '@eslint/compat@1.3.1(eslint@9.30.1)': optionalDependencies: - eslint: 9.29.0 + eslint: 9.30.1 - '@eslint/config-array@0.20.1': + '@eslint/config-array@0.21.0': dependencies: '@eslint/object-schema': 2.1.6 debug: 4.4.1(supports-color@8.1.1) @@ -4877,13 +4763,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.2.3': {} + '@eslint/config-helpers@0.3.0': {} '@eslint/core@0.14.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/core@0.15.0': + '@eslint/core@0.15.1': dependencies: '@types/json-schema': 7.0.15 @@ -4901,18 +4787,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.29.0': {} - - '@eslint/js@9.30.0': {} + '@eslint/js@9.30.1': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.3.2': + '@eslint/plugin-kit@0.3.3': dependencies: - '@eslint/core': 0.15.0 + '@eslint/core': 0.15.1 levn: 0.4.1 - '@faker-js/faker@9.8.0': {} + '@faker-js/faker@9.9.0': {} '@humanfs/core@0.19.1': {} @@ -4976,7 +4860,7 @@ snapshots: '@uiw/react-textarea-code-editor': 3.1.1(@babel/runtime@7.27.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) file-saver: 2.0.5 flat: 6.0.1 - i18next: 25.2.1(typescript@5.8.3) + i18next: 25.3.1(typescript@5.8.3) i18next-fetch-backend: 6.0.0 jszip: 3.10.1 keycloak-js: 26.2.0 @@ -4984,8 +4868,8 @@ snapshots: p-debounce: 4.0.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-hook-form: 7.59.0(react@18.3.1) - react-i18next: 15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + react-hook-form: 7.60.0(react@18.3.1) + react-i18next: 15.5.3(i18next@25.3.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) react-router-dom: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) reactflow: 11.11.4(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) use-react-router-breadcrumbs: 4.0.1(react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) @@ -5004,23 +4888,23 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} - '@microsoft/api-extractor-model@7.30.6(@types/node@24.0.4)': + '@microsoft/api-extractor-model@7.30.6(@types/node@24.0.10)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.13.1(@types/node@24.0.4) + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.10) transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.52.8(@types/node@24.0.4)': + '@microsoft/api-extractor@7.52.8(@types/node@24.0.10)': dependencies: - '@microsoft/api-extractor-model': 7.30.6(@types/node@24.0.4) + '@microsoft/api-extractor-model': 7.30.6(@types/node@24.0.10) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.13.1(@types/node@24.0.4) + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.10) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.3(@types/node@24.0.4) - '@rushstack/ts-command-line': 5.0.1(@types/node@24.0.4) + '@rushstack/terminal': 0.15.3(@types/node@24.0.10) + '@rushstack/ts-command-line': 5.0.1(@types/node@24.0.10) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.10 @@ -5155,9 +5039,9 @@ snapshots: '@pkgr/core@0.2.7': {} - '@playwright/test@1.53.1': + '@playwright/test@1.53.2': dependencies: - playwright: 1.53.1 + playwright: 1.53.2 '@reactflow/background@11.3.14(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -5241,9 +5125,9 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.11': {} - '@rollup/plugin-commonjs@28.0.6(rollup@4.44.1)': + '@rollup/plugin-commonjs@28.0.6(rollup@4.44.2)': dependencies: - '@rollup/pluginutils': 5.2.0(rollup@4.44.1) + '@rollup/pluginutils': 5.2.0(rollup@4.44.2) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.4.6(picomatch@4.0.2) @@ -5251,170 +5135,110 @@ snapshots: magic-string: 0.30.17 picomatch: 4.0.2 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/plugin-node-resolve@16.0.1(rollup@4.44.1)': + '@rollup/plugin-node-resolve@16.0.1(rollup@4.44.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.44.1) + '@rollup/pluginutils': 5.1.4(rollup@4.44.2) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.10 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/plugin-replace@6.0.2(rollup@4.44.1)': + '@rollup/plugin-replace@6.0.2(rollup@4.44.2)': dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.44.1) + '@rollup/pluginutils': 5.1.4(rollup@4.44.2) magic-string: 0.30.17 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/plugin-terser@0.4.4(rollup@4.44.1)': + '@rollup/plugin-terser@0.4.4(rollup@4.44.2)': dependencies: serialize-javascript: 6.0.2 smob: 1.5.0 terser: 5.39.0 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/pluginutils@5.1.4(rollup@4.44.1)': + '@rollup/pluginutils@5.1.4(rollup@4.44.2)': dependencies: '@types/estree': 1.0.7 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/pluginutils@5.2.0(rollup@4.44.1)': + '@rollup/pluginutils@5.2.0(rollup@4.44.2)': dependencies: '@types/estree': 1.0.8 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.44.1 + rollup: 4.44.2 - '@rollup/rollup-android-arm-eabi@4.44.0': + '@rollup/rollup-android-arm-eabi@4.44.2': optional: true - '@rollup/rollup-android-arm-eabi@4.44.1': + '@rollup/rollup-android-arm64@4.44.2': optional: true - '@rollup/rollup-android-arm64@4.44.0': + '@rollup/rollup-darwin-arm64@4.44.2': optional: true - '@rollup/rollup-android-arm64@4.44.1': + '@rollup/rollup-darwin-x64@4.44.2': optional: true - '@rollup/rollup-darwin-arm64@4.44.0': + '@rollup/rollup-freebsd-arm64@4.44.2': optional: true - '@rollup/rollup-darwin-arm64@4.44.1': + '@rollup/rollup-freebsd-x64@4.44.2': optional: true - '@rollup/rollup-darwin-x64@4.44.0': + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': optional: true - '@rollup/rollup-darwin-x64@4.44.1': + '@rollup/rollup-linux-arm-musleabihf@4.44.2': optional: true - '@rollup/rollup-freebsd-arm64@4.44.0': + '@rollup/rollup-linux-arm64-gnu@4.44.2': optional: true - '@rollup/rollup-freebsd-arm64@4.44.1': + '@rollup/rollup-linux-arm64-musl@4.44.2': optional: true - '@rollup/rollup-freebsd-x64@4.44.0': + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': optional: true - '@rollup/rollup-freebsd-x64@4.44.1': + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.44.0': + '@rollup/rollup-linux-riscv64-gnu@4.44.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + '@rollup/rollup-linux-riscv64-musl@4.44.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.44.0': + '@rollup/rollup-linux-s390x-gnu@4.44.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.44.1': + '@rollup/rollup-linux-x64-gnu@4.44.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.44.0': + '@rollup/rollup-linux-x64-musl@4.44.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.44.1': + '@rollup/rollup-win32-arm64-msvc@4.44.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.44.0': + '@rollup/rollup-win32-ia32-msvc@4.44.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.44.1': + '@rollup/rollup-win32-x64-msvc@4.44.2': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.44.0': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.44.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.44.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.44.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.44.1': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.44.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.44.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.44.1': - optional: true - - '@rollup/rollup-linux-x64-musl@4.44.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.44.1': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.44.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.44.1': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.44.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.44.1': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.44.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.44.1': - optional: true - - '@rushstack/node-core-library@5.13.1(@types/node@24.0.4)': + '@rushstack/node-core-library@5.13.1(@types/node@24.0.10)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -5425,23 +5249,23 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 24.0.4 + '@types/node': 24.0.10 '@rushstack/rig-package@0.5.3': dependencies: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.15.3(@types/node@24.0.4)': + '@rushstack/terminal@0.15.3(@types/node@24.0.10)': dependencies: - '@rushstack/node-core-library': 5.13.1(@types/node@24.0.4) + '@rushstack/node-core-library': 5.13.1(@types/node@24.0.10) supports-color: 8.1.1 optionalDependencies: - '@types/node': 24.0.4 + '@types/node': 24.0.10 - '@rushstack/ts-command-line@5.0.1(@types/node@24.0.4)': + '@rushstack/ts-command-line@5.0.1(@types/node@24.0.10)': dependencies: - '@rushstack/terminal': 0.15.3(@types/node@24.0.4) + '@rushstack/terminal': 0.15.3(@types/node@24.0.10) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -5664,7 +5488,7 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 - '@types/dagre@0.7.52': {} + '@types/dagre@0.7.53': {} '@types/deep-eql@4.0.2': {} @@ -5706,7 +5530,7 @@ snapshots: dependencies: undici-types: 6.20.0 - '@types/node@24.0.4': + '@types/node@24.0.10': dependencies: undici-types: 7.8.0 @@ -5732,21 +5556,21 @@ snapshots: '@types/tar-stream@3.1.3': dependencies: - '@types/node': 24.0.4 + '@types/node': 24.0.10 '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} - '@typescript-eslint/eslint-plugin@8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/eslint-plugin@8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1)(typescript@5.8.3))(eslint@9.30.1)(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/type-utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.35.0 - eslint: 9.29.0 + '@typescript-eslint/parser': 8.36.0(eslint@9.30.1)(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/type-utils': 8.36.0(eslint@9.30.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.30.1)(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.36.0 + eslint: 9.30.1 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -5755,55 +5579,55 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/parser@8.36.0(eslint@9.30.1)(typescript@5.8.3)': dependencies: - '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.35.0 + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.36.0 debug: 4.4.1(supports-color@8.1.1) - eslint: 9.29.0 + eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.35.0(typescript@5.8.3)': + '@typescript-eslint/project-service@8.36.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) + '@typescript-eslint/types': 8.36.0 debug: 4.4.1(supports-color@8.1.1) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.35.0': + '@typescript-eslint/scope-manager@8.36.0': dependencies: - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 - '@typescript-eslint/tsconfig-utils@8.35.0(typescript@5.8.3)': + '@typescript-eslint/tsconfig-utils@8.36.0(typescript@5.8.3)': dependencies: typescript: 5.8.3 - '@typescript-eslint/type-utils@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/type-utils@8.36.0(eslint@9.30.1)(typescript@5.8.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.30.1)(typescript@5.8.3) debug: 4.4.1(supports-color@8.1.1) - eslint: 9.29.0 + eslint: 9.30.1 ts-api-utils: 2.1.0(typescript@5.8.3) typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.35.0': {} + '@typescript-eslint/types@8.36.0': {} - '@typescript-eslint/typescript-estree@8.35.0(typescript@5.8.3)': + '@typescript-eslint/typescript-estree@8.36.0(typescript@5.8.3)': dependencies: - '@typescript-eslint/project-service': 8.35.0(typescript@5.8.3) - '@typescript-eslint/tsconfig-utils': 8.35.0(typescript@5.8.3) - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/visitor-keys': 8.35.0 + '@typescript-eslint/project-service': 8.36.0(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.36.0(typescript@5.8.3) + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/visitor-keys': 8.36.0 debug: 4.4.1(supports-color@8.1.1) fast-glob: 3.3.3 is-glob: 4.0.3 @@ -5814,20 +5638,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.35.0(eslint@9.29.0)(typescript@5.8.3)': + '@typescript-eslint/utils@8.36.0(eslint@9.30.1)(typescript@5.8.3)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0) - '@typescript-eslint/scope-manager': 8.35.0 - '@typescript-eslint/types': 8.35.0 - '@typescript-eslint/typescript-estree': 8.35.0(typescript@5.8.3) - eslint: 9.29.0 + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1) + '@typescript-eslint/scope-manager': 8.36.0 + '@typescript-eslint/types': 8.36.0 + '@typescript-eslint/typescript-estree': 8.36.0(typescript@5.8.3) + eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.35.0': + '@typescript-eslint/visitor-keys@8.36.0': dependencies: - '@typescript-eslint/types': 8.35.0 + '@typescript-eslint/types': 8.36.0 eslint-visitor-keys: 4.2.1 '@uiw/react-textarea-code-editor@3.1.1(@babel/runtime@7.27.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': @@ -5840,11 +5664,11 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react-swc@3.10.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': + '@vitejs/plugin-react-swc@3.10.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.11 '@swc/core': 1.12.0 - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - '@swc/helpers' @@ -5856,13 +5680,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': + '@vitest/mocker@3.2.4(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -6132,6 +5956,11 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + brace-expansion@2.0.2: dependencies: balanced-match: 1.0.2 @@ -6425,7 +6254,7 @@ snapshots: duplexify@3.7.1: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 inherits: 2.0.4 readable-stream: 2.3.8 stream-shift: 1.0.3 @@ -6440,10 +6269,6 @@ snapshots: emoji-regex@9.2.2: {} - end-of-stream@1.4.4: - dependencies: - once: 1.4.0 - end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -6554,78 +6379,79 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.5: + esbuild@0.25.6: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.5 - '@esbuild/android-arm': 0.25.5 - '@esbuild/android-arm64': 0.25.5 - '@esbuild/android-x64': 0.25.5 - '@esbuild/darwin-arm64': 0.25.5 - '@esbuild/darwin-x64': 0.25.5 - '@esbuild/freebsd-arm64': 0.25.5 - '@esbuild/freebsd-x64': 0.25.5 - '@esbuild/linux-arm': 0.25.5 - '@esbuild/linux-arm64': 0.25.5 - '@esbuild/linux-ia32': 0.25.5 - '@esbuild/linux-loong64': 0.25.5 - '@esbuild/linux-mips64el': 0.25.5 - '@esbuild/linux-ppc64': 0.25.5 - '@esbuild/linux-riscv64': 0.25.5 - '@esbuild/linux-s390x': 0.25.5 - '@esbuild/linux-x64': 0.25.5 - '@esbuild/netbsd-arm64': 0.25.5 - '@esbuild/netbsd-x64': 0.25.5 - '@esbuild/openbsd-arm64': 0.25.5 - '@esbuild/openbsd-x64': 0.25.5 - '@esbuild/sunos-x64': 0.25.5 - '@esbuild/win32-arm64': 0.25.5 - '@esbuild/win32-ia32': 0.25.5 - '@esbuild/win32-x64': 0.25.5 + '@esbuild/aix-ppc64': 0.25.6 + '@esbuild/android-arm': 0.25.6 + '@esbuild/android-arm64': 0.25.6 + '@esbuild/android-x64': 0.25.6 + '@esbuild/darwin-arm64': 0.25.6 + '@esbuild/darwin-x64': 0.25.6 + '@esbuild/freebsd-arm64': 0.25.6 + '@esbuild/freebsd-x64': 0.25.6 + '@esbuild/linux-arm': 0.25.6 + '@esbuild/linux-arm64': 0.25.6 + '@esbuild/linux-ia32': 0.25.6 + '@esbuild/linux-loong64': 0.25.6 + '@esbuild/linux-mips64el': 0.25.6 + '@esbuild/linux-ppc64': 0.25.6 + '@esbuild/linux-riscv64': 0.25.6 + '@esbuild/linux-s390x': 0.25.6 + '@esbuild/linux-x64': 0.25.6 + '@esbuild/netbsd-arm64': 0.25.6 + '@esbuild/netbsd-x64': 0.25.6 + '@esbuild/openbsd-arm64': 0.25.6 + '@esbuild/openbsd-x64': 0.25.6 + '@esbuild/openharmony-arm64': 0.25.6 + '@esbuild/sunos-x64': 0.25.6 + '@esbuild/win32-arm64': 0.25.6 + '@esbuild/win32-ia32': 0.25.6 + '@esbuild/win32-x64': 0.25.6 escalade@3.2.0: {} escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.5(eslint@9.29.0): + eslint-config-prettier@10.1.5(eslint@9.30.1): dependencies: - eslint: 9.29.0 + eslint: 9.30.1 - eslint-plugin-lodash@8.0.0(eslint@9.29.0): + eslint-plugin-lodash@8.0.0(eslint@9.30.1): dependencies: - eslint: 9.29.0 + eslint: 9.30.1 lodash: 4.17.21 - eslint-plugin-playwright@2.2.0(eslint@9.29.0): + eslint-plugin-playwright@2.2.0(eslint@9.30.1): dependencies: - eslint: 9.29.0 + eslint: 9.30.1 globals: 13.24.0 - eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@9.29.0))(eslint@9.29.0)(prettier@3.6.2): + eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1))(eslint@9.30.1)(prettier@3.6.2): dependencies: - eslint: 9.29.0 + eslint: 9.30.1 prettier: 3.6.2 prettier-linter-helpers: 1.0.0 synckit: 0.11.8 optionalDependencies: - eslint-config-prettier: 10.1.5(eslint@9.29.0) + eslint-config-prettier: 10.1.5(eslint@9.30.1) - eslint-plugin-react-compiler@19.0.0-beta-714736e-20250131(eslint@9.29.0): + eslint-plugin-react-compiler@19.0.0-beta-714736e-20250131(eslint@9.30.1): dependencies: '@babel/core': 7.26.9 '@babel/parser': 7.26.9 '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.9) - eslint: 9.29.0 + eslint: 9.30.1 hermes-parser: 0.25.1 zod: 3.24.2 zod-validation-error: 3.4.0(zod@3.24.2) transitivePeerDependencies: - supports-color - eslint-plugin-react-hooks@5.2.0(eslint@9.29.0): + eslint-plugin-react-hooks@5.2.0(eslint@9.30.1): dependencies: - eslint: 9.29.0 + eslint: 9.30.1 - eslint-plugin-react@7.37.5(eslint@9.29.0): + eslint-plugin-react@7.37.5(eslint@9.30.1): dependencies: array-includes: 3.1.8 array.prototype.findlast: 1.2.5 @@ -6633,7 +6459,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.29.0 + eslint: 9.30.1 estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -6654,20 +6480,18 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.0: {} - eslint-visitor-keys@4.2.1: {} - eslint@9.29.0: + eslint@9.30.1: dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.29.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.1 - '@eslint/config-helpers': 0.2.3 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 '@eslint/core': 0.14.0 '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.29.0 - '@eslint/plugin-kit': 0.3.2 + '@eslint/js': 9.30.1 + '@eslint/plugin-kit': 0.3.3 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -6702,7 +6526,7 @@ snapshots: dependencies: acorn: 8.14.1 acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 + eslint-visitor-keys: 4.2.1 espree@10.4.0: dependencies: @@ -7038,9 +6862,9 @@ snapshots: i18next-fetch-backend@6.0.0: {} - i18next@25.2.1(typescript@5.8.3): + i18next@25.3.1(typescript@5.8.3): dependencies: - '@babel/runtime': 7.27.1 + '@babel/runtime': 7.27.6 optionalDependencies: typescript: 5.8.3 @@ -7504,7 +7328,7 @@ snapshots: minimatch@3.1.2: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 1.1.12 minimatch@9.0.5: dependencies: @@ -7715,11 +7539,11 @@ snapshots: exsolve: 1.0.5 pathe: 2.0.3 - playwright-core@1.53.1: {} + playwright-core@1.53.2: {} - playwright@1.53.1: + playwright@1.53.2: dependencies: - playwright-core: 1.53.1 + playwright-core: 1.53.2 optionalDependencies: fsevents: 2.3.2 @@ -7767,7 +7591,7 @@ snapshots: pump@2.0.1: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 pump@3.0.3: @@ -7804,15 +7628,15 @@ snapshots: prop-types: 15.8.1 react: 18.3.1 - react-hook-form@7.59.0(react@18.3.1): + react-hook-form@7.60.0(react@18.3.1): dependencies: react: 18.3.1 - react-i18next@15.5.3(i18next@25.2.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + react-i18next@15.5.3(i18next@25.3.1(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): dependencies: '@babel/runtime': 7.27.6 html-parse-stringify: 3.0.1 - i18next: 25.2.1(typescript@5.8.3) + i18next: 25.3.1(typescript@5.8.3) react: 18.3.1 optionalDependencies: react-dom: 18.3.1(react@18.3.1) @@ -7964,60 +7788,34 @@ snapshots: rfdc@1.4.1: {} - rollup-plugin-peer-deps-external@2.2.4(rollup@4.44.1): - dependencies: - rollup: 4.44.1 - - rollup@4.44.0: + rollup-plugin-peer-deps-external@2.2.4(rollup@4.44.2): dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.0 - '@rollup/rollup-android-arm64': 4.44.0 - '@rollup/rollup-darwin-arm64': 4.44.0 - '@rollup/rollup-darwin-x64': 4.44.0 - '@rollup/rollup-freebsd-arm64': 4.44.0 - '@rollup/rollup-freebsd-x64': 4.44.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.0 - '@rollup/rollup-linux-arm-musleabihf': 4.44.0 - '@rollup/rollup-linux-arm64-gnu': 4.44.0 - '@rollup/rollup-linux-arm64-musl': 4.44.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.0 - '@rollup/rollup-linux-riscv64-gnu': 4.44.0 - '@rollup/rollup-linux-riscv64-musl': 4.44.0 - '@rollup/rollup-linux-s390x-gnu': 4.44.0 - '@rollup/rollup-linux-x64-gnu': 4.44.0 - '@rollup/rollup-linux-x64-musl': 4.44.0 - '@rollup/rollup-win32-arm64-msvc': 4.44.0 - '@rollup/rollup-win32-ia32-msvc': 4.44.0 - '@rollup/rollup-win32-x64-msvc': 4.44.0 - fsevents: 2.3.3 + rollup: 4.44.2 - rollup@4.44.1: + rollup@4.44.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.44.1 - '@rollup/rollup-android-arm64': 4.44.1 - '@rollup/rollup-darwin-arm64': 4.44.1 - '@rollup/rollup-darwin-x64': 4.44.1 - '@rollup/rollup-freebsd-arm64': 4.44.1 - '@rollup/rollup-freebsd-x64': 4.44.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 - '@rollup/rollup-linux-arm-musleabihf': 4.44.1 - '@rollup/rollup-linux-arm64-gnu': 4.44.1 - '@rollup/rollup-linux-arm64-musl': 4.44.1 - '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 - '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-gnu': 4.44.1 - '@rollup/rollup-linux-riscv64-musl': 4.44.1 - '@rollup/rollup-linux-s390x-gnu': 4.44.1 - '@rollup/rollup-linux-x64-gnu': 4.44.1 - '@rollup/rollup-linux-x64-musl': 4.44.1 - '@rollup/rollup-win32-arm64-msvc': 4.44.1 - '@rollup/rollup-win32-ia32-msvc': 4.44.1 - '@rollup/rollup-win32-x64-msvc': 4.44.1 + '@rollup/rollup-android-arm-eabi': 4.44.2 + '@rollup/rollup-android-arm64': 4.44.2 + '@rollup/rollup-darwin-arm64': 4.44.2 + '@rollup/rollup-darwin-x64': 4.44.2 + '@rollup/rollup-freebsd-arm64': 4.44.2 + '@rollup/rollup-freebsd-x64': 4.44.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 + '@rollup/rollup-linux-arm-musleabihf': 4.44.2 + '@rollup/rollup-linux-arm64-gnu': 4.44.2 + '@rollup/rollup-linux-arm64-musl': 4.44.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-musl': 4.44.2 + '@rollup/rollup-linux-s390x-gnu': 4.44.2 + '@rollup/rollup-linux-x64-gnu': 4.44.2 + '@rollup/rollup-linux-x64-musl': 4.44.2 + '@rollup/rollup-win32-arm64-msvc': 4.44.2 + '@rollup/rollup-win32-ia32-msvc': 4.44.2 + '@rollup/rollup-win32-x64-msvc': 4.44.2 fsevents: 2.3.3 rrweb-cssom@0.8.0: {} @@ -8290,7 +8088,7 @@ snapshots: tabbable@6.2.0: {} - tar-fs@3.0.10: + tar-fs@3.1.0: dependencies: pump: 3.0.3 tar-stream: 3.1.7 @@ -8328,11 +8126,6 @@ snapshots: tinyexec@0.3.2: {} - tinyglobby@0.2.13: - dependencies: - fdir: 6.4.6(picomatch@4.0.2) - picomatch: 4.0.2 - tinyglobby@0.2.14: dependencies: fdir: 6.4.6(picomatch@4.0.2) @@ -8370,14 +8163,14 @@ snapshots: dependencies: typescript: 5.8.3 - ts-node@10.9.2(@swc/core@1.12.0)(@types/node@24.0.4)(typescript@5.8.3): + ts-node@10.9.2(@swc/core@1.12.0)(@types/node@24.0.10)(typescript@5.8.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 24.0.4 + '@types/node': 24.0.10 acorn: 8.14.0 acorn-walk: 8.3.4 arg: 4.1.3 @@ -8394,7 +8187,7 @@ snapshots: tsx@4.19.3: dependencies: - esbuild: 0.25.5 + esbuild: 0.25.6 get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 @@ -8439,12 +8232,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.35.0(eslint@9.29.0)(typescript@5.8.3): + typescript-eslint@8.36.0(eslint@9.30.1)(typescript@5.8.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.35.0(@typescript-eslint/parser@8.35.0(eslint@9.29.0)(typescript@5.8.3))(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.35.0(eslint@9.29.0)(typescript@5.8.3) - eslint: 9.29.0 + '@typescript-eslint/eslint-plugin': 8.36.0(@typescript-eslint/parser@8.36.0(eslint@9.30.1)(typescript@5.8.3))(eslint@9.30.1)(typescript@5.8.3) + '@typescript-eslint/parser': 8.36.0(eslint@9.30.1)(typescript@5.8.3) + '@typescript-eslint/utils': 8.36.0(eslint@9.30.1)(typescript@5.8.3) + eslint: 9.30.1 typescript: 5.8.3 transitivePeerDependencies: - supports-color @@ -8555,13 +8348,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-node@3.2.4(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): + vite-node@3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): dependencies: cac: 6.7.14 debug: 4.4.1(supports-color@8.1.1) es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - jiti @@ -8576,7 +8369,7 @@ snapshots: - tsx - yaml - vite-plugin-checker@0.9.3(eslint@9.29.0)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): + vite-plugin-checker@0.10.0(eslint@9.30.1)(optionator@0.9.4)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): dependencies: '@babel/code-frame': 7.27.1 chokidar: 4.0.3 @@ -8585,18 +8378,18 @@ snapshots: picomatch: 4.0.2 strip-ansi: 7.1.0 tiny-invariant: 1.3.3 - tinyglobby: 0.2.13 - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + tinyglobby: 0.2.14 + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) vscode-uri: 3.1.0 optionalDependencies: - eslint: 9.29.0 + eslint: 9.30.1 optionator: 0.9.4 typescript: 5.8.3 - vite-plugin-dts@4.5.4(@types/node@24.0.4)(rollup@4.44.1)(typescript@5.8.3)(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@24.0.10)(rollup@4.44.2)(typescript@5.8.3)(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): dependencies: - '@microsoft/api-extractor': 7.52.8(@types/node@24.0.4) - '@rollup/pluginutils': 5.1.4(rollup@4.44.1) + '@microsoft/api-extractor': 7.52.8(@types/node@24.0.10) + '@rollup/pluginutils': 5.1.4(rollup@4.44.2) '@volar/typescript': 2.4.14 '@vue/language-core': 2.2.0(typescript@5.8.3) compare-versions: 6.1.1 @@ -8606,40 +8399,40 @@ snapshots: magic-string: 0.30.17 typescript: 5.8.3 optionalDependencies: - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-lib-inject-css@2.2.2(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): + vite-plugin-lib-inject-css@2.2.2(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)): dependencies: '@ast-grep/napi': 0.36.2 magic-string: 0.30.17 picocolors: 1.1.1 - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) - vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): + vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): dependencies: - esbuild: 0.25.5 + esbuild: 0.25.6 fdir: 6.4.6(picomatch@4.0.2) picomatch: 4.0.2 postcss: 8.5.6 - rollup: 4.44.0 + rollup: 4.44.2 tinyglobby: 0.2.14 optionalDependencies: - '@types/node': 24.0.4 + '@types/node': 24.0.10 fsevents: 2.3.3 lightningcss: 1.30.1 terser: 5.39.0 tsx: 4.19.3 yaml: 2.8.0 - vitest@3.2.4(@types/node@24.0.4)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): + vitest@3.2.4(@types/node@24.0.10)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) + '@vitest/mocker': 3.2.4(vite@7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -8657,11 +8450,11 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.0.0(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) - vite-node: 3.2.4(@types/node@24.0.4)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite: 7.0.3(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) + vite-node: 3.2.4(@types/node@24.0.10)(lightningcss@1.30.1)(terser@5.39.0)(tsx@4.19.3)(yaml@2.8.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.0.4 + '@types/node': 24.0.10 jsdom: 26.1.0 transitivePeerDependencies: - jiti diff --git a/js/themes-vendor/package.json b/js/themes-vendor/package.json index 330b126b95f6..a0fe409bdb0c 100644 --- a/js/themes-vendor/package.json +++ b/js/themes-vendor/package.json @@ -34,6 +34,6 @@ "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-replace": "^6.0.2", "@rollup/plugin-terser": "^0.4.4", - "rollup": "^4.44.1" + "rollup": "^4.44.2" } } diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CachedCount.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CachedCount.java index 758d72cd2ac2..24a230eecf30 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CachedCount.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/CachedCount.java @@ -22,18 +22,18 @@ public class CachedCount extends AbstractRevisioned implements InRealm { - private final RealmModel realm; + private final String realm; private final long count; public CachedCount(Long revision, RealmModel realm, String cacheKey, long count) { super(revision, cacheKey); - this.realm = realm; + this.realm = realm.getId(); this.count = count; } @Override public String getRealm() { - return realm.getId(); + return realm; } public long getCount() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/organization/CachedMembership.java b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/organization/CachedMembership.java index ec20218deee4..fda8a147d19f 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/organization/CachedMembership.java +++ b/model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/organization/CachedMembership.java @@ -23,20 +23,20 @@ public class CachedMembership extends AbstractRevisioned implements InRealm { - private final RealmModel realm; + private final String realm; private final boolean managed; private final boolean isMember; public CachedMembership(Long revision, String key, RealmModel realm, boolean managed, boolean isMember) { super(revision, key); - this.realm = realm; + this.realm = realm.getId(); this.managed = managed; this.isMember = isMember; } @Override public String getRealm() { - return realm.getId(); + return realm; } public boolean isManaged() { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java index 7495862f27da..541e57f70107 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProvider.java @@ -18,7 +18,6 @@ package org.keycloak.models.sessions.infinispan; import java.util.Collection; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; @@ -29,7 +28,6 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -37,9 +35,6 @@ import java.util.stream.StreamSupport; import org.infinispan.Cache; -import org.infinispan.client.hotrod.RemoteCache; -import org.infinispan.client.hotrod.exceptions.HotRodClientException; -import org.infinispan.commons.api.BasicCache; import org.infinispan.commons.util.concurrent.CompletionStages; import org.infinispan.stream.CacheCollectors; import org.jboss.logging.Logger; @@ -95,11 +90,6 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi protected final KeycloakSession session; - protected final Cache> sessionCache; - protected final Cache> offlineSessionCache; - protected final Cache> clientSessionCache; - protected final Cache> offlineClientSessionCache; - protected final InfinispanChangelogBasedTransaction sessionTx; protected final InfinispanChangelogBasedTransaction offlineSessionTx; protected final InfinispanChangelogBasedTransaction clientSessionTx; @@ -111,9 +101,9 @@ public class InfinispanUserSessionProvider implements UserSessionProvider, Sessi protected final InfinispanKeyGenerator keyGenerator; - protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster; + protected final SessionFunction offlineSessionCacheEntryLifespanAdjuster; - protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster; + protected final SessionFunction offlineClientSessionCacheEntryLifespanAdjuster; public InfinispanUserSessionProvider(KeycloakSession session, PersisterLastSessionRefreshStore persisterLastSessionRefreshStore, @@ -130,11 +120,6 @@ public InfinispanUserSessionProvider(KeycloakSession session, SerializeExecutionsByKey serializerOfflineClientSession) { this.session = session; - this.sessionCache = sessionCache; - this.clientSessionCache = clientSessionCache; - this.offlineSessionCache = offlineSessionCache; - this.offlineClientSessionCache = offlineClientSessionCache; - this.sessionTx = new InfinispanChangelogBasedTransaction<>(session, sessionCache, SessionTimeouts::getUserSessionLifespanMs, SessionTimeouts::getUserSessionMaxIdleMs, serializerSession); this.offlineSessionTx = new InfinispanChangelogBasedTransaction<>(session, offlineSessionCache, offlineSessionCacheEntryLifespanAdjuster, SessionTimeouts::getOfflineSessionMaxIdleMs, serializerOfflineSession); this.clientSessionTx = new InfinispanChangelogBasedTransaction<>(session, clientSessionCache, SessionTimeouts::getClientSessionLifespanMs, SessionTimeouts::getClientSessionMaxIdleMs, serializerClientSession); @@ -155,7 +140,7 @@ public InfinispanUserSessionProvider(KeycloakSession session, } protected Cache> getCache(boolean offline) { - return offline ? offlineSessionCache : sessionCache; + return offline ? offlineSessionTx.getCache() : sessionTx.getCache(); } protected InfinispanChangelogBasedTransaction getTransaction(boolean offline) { @@ -163,7 +148,7 @@ protected InfinispanChangelogBasedTransaction getTran } protected Cache> getClientSessionCache(boolean offline) { - return offline ? offlineClientSessionCache : clientSessionCache; + return offline ? offlineClientSessionTx.getCache() : clientSessionTx.getCache(); } protected InfinispanChangelogBasedTransaction getClientSessionTransaction(boolean offline) { @@ -182,11 +167,10 @@ public KeycloakSession getKeycloakSession() { @Override public AuthenticatedClientSessionModel createClientSession(RealmModel realm, ClientModel client, UserSessionModel userSession) { - final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionCache); + InfinispanChangelogBasedTransaction clientSessionUpdateTx = clientSessionTx; + final UUID clientSessionId = keyGenerator.generateKeyUUID(session, clientSessionUpdateTx.getCache()); var entity = AuthenticatedClientSessionEntity.create(clientSessionId, realm, client, userSession); - InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(false); - InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(false); AuthenticatedClientSessionAdapter adapter = new AuthenticatedClientSessionAdapter(session, entity, client, userSession, clientSessionUpdateTx, false); // For now, the clientSession is considered transient in case that userSession was transient @@ -196,8 +180,7 @@ public AuthenticatedClientSessionModel createClientSession(RealmModel realm, Cli SessionUpdateTask createClientSessionTask = Tasks.addIfAbsentSync(); clientSessionUpdateTx.addTask(clientSessionId, createClientSessionTask, entity, persistenceState); - SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(client.getId(), clientSessionId); - userSessionUpdateTx.addTask(userSession.getId(), registerClientSessionTask); + sessionTx.addTask(userSession.getId(), new RegisterClientSessionTask(client.getId(), clientSessionId)); return adapter; } @@ -206,7 +189,7 @@ public AuthenticatedClientSessionModel createClientSession(RealmModel realm, Cli public UserSessionModel createUserSession(String id, RealmModel realm, UserModel user, String loginUsername, String ipAddress, String authMethod, boolean rememberMe, String brokerSessionId, String brokerUserId, UserSessionModel.SessionPersistenceState persistenceState) { if (id == null) { - id = keyGenerator.generateKeyString(session, sessionCache); + id = keyGenerator.generateKeyString(session, sessionTx.getCache()); } UserSessionEntity entity = UserSessionEntity.create(id, realm, user, loginUsername, ipAddress, authMethod, rememberMe, brokerSessionId, brokerUserId); @@ -214,7 +197,7 @@ public UserSessionModel createUserSession(String id, RealmModel realm, UserModel SessionUpdateTask createSessionTask = Tasks.addIfAbsentSync(); sessionTx.addTask(id, createSessionTask, entity, persistenceState); - UserSessionAdapter adapter = user instanceof LightweightUserAdapter + UserSessionAdapter adapter = user instanceof LightweightUserAdapter ? wrap(realm, entity, false, user) : wrap(realm, entity, false); adapter.setPersistenceState(persistenceState); @@ -237,7 +220,7 @@ public void migrate(String modelVersion) { } } - protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { + protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean offline) { UserSessionEntity userSessionEntityFromCache = getUserSessionEntity(realm, id, offline); if (userSessionEntityFromCache != null) { @@ -250,63 +233,100 @@ protected UserSessionAdapter getUserSession(RealmModel realm, String id, boolean // Try to recover from potentially lost offline-sessions by attempting to fetch and re-import // the offline session information from the PersistenceProvider. - UserSessionEntity userSessionEntityFromPersistenceProvider = getUserSessionEntityFromPersistenceProvider(realm, id, offline); + UserSessionEntity userSessionEntityFromPersistenceProvider = getUserSessionEntityFromPersistenceProvider(realm, id); if (userSessionEntityFromPersistenceProvider != null) { // we successfully recovered the offline session! - return wrap(realm, userSessionEntityFromPersistenceProvider, offline); + return wrap(realm, userSessionEntityFromPersistenceProvider, true); } // no luck, the session is really not there anymore return null; } - private UserSessionEntity getUserSessionEntityFromPersistenceProvider(RealmModel realm, String sessionId, boolean offline) { - + private UserSessionEntity getUserSessionEntityFromPersistenceProvider(RealmModel realm, String sessionId) { log.debugf("Offline user-session not found in infinispan, attempting UserSessionPersisterProvider lookup for sessionId=%s", sessionId); UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - UserSessionModel persistentUserSession = persister.loadUserSession(realm, sessionId, offline); + UserSessionModel persistentUserSession = persister.loadUserSession(realm, sessionId, true); if (persistentUserSession == null) { log.debugf("Offline user-session not found in UserSessionPersisterProvider for sessionId=%s", sessionId); return null; } - UserSessionEntity sessionEntity = importUserSession(realm, offline, persistentUserSession); + UserSessionEntity sessionEntity = importUserSession(realm, persistentUserSession); if (sessionEntity == null) { - persister.removeUserSession(sessionId, offline); + // TODO session expired, remove or ignore? + persister.removeUserSession(sessionId, true); } return sessionEntity; } - private UserSessionEntity getUserSessionEntityFromCacheOrImportIfNecessary(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) { - - UserSessionEntity userSessionEntity = getUserSessionEntity(realm, persistentUserSession.getId(), offline); + private UserSessionEntity getUserSessionEntityFromCacheOrImportIfNecessary(RealmModel realm, UserSessionModel persistentUserSession) { + UserSessionEntity userSessionEntity = getUserSessionEntity(realm, persistentUserSession.getId(), true); if (userSessionEntity != null) { // user session present in cache, return existing session return userSessionEntity; } - return importUserSession(realm, offline, persistentUserSession); + return importUserSession(realm, persistentUserSession); } - private UserSessionEntity importUserSession(RealmModel realm, boolean offline, UserSessionModel persistentUserSession) { - + private UserSessionEntity importUserSession(RealmModel realm, UserSessionModel persistentUserSession) { String sessionId = persistentUserSession.getId(); - log.debugf("Attempting to import user-session for sessionId=%s offline=%s", sessionId, offline); - session.sessions().importUserSessions(Collections.singleton(persistentUserSession), offline); - log.debugf("user-session imported, trying another lookup for sessionId=%s offline=%s", sessionId, offline); + log.debugf("Attempting to import user-session for sessionId=%s offline=true", sessionId); - UserSessionEntity ispnUserSessionEntity = getUserSessionEntity(realm, sessionId, offline); + var userSessionEntityToImport = UserSessionEntity.createFromModel(persistentUserSession); - if (ispnUserSessionEntity != null) { - log.debugf("user-session found after import for sessionId=%s offline=%s", sessionId, offline); - return ispnUserSessionEntity; + long lifespan = offlineSessionCacheEntryLifespanAdjuster.apply(realm, null, userSessionEntityToImport); + long maxIdle = SessionTimeouts.getOfflineSessionMaxIdleMs(realm, null, userSessionEntityToImport); + + if (lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG || maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG) { + log.debugf("Session has expired. Do not import user-session for sessionId=%s offline=true", sessionId); + return null; } - log.debugf("user-session could not be found after import for sessionId=%s offline=%s", sessionId, offline); - return null; + UserSessionEntity existing = getTransaction(true) + .importSession(realm, userSessionEntityToImport.getId(), new SessionEntityWrapper<>(userSessionEntityToImport), + lifespan, maxIdle); + + if (existing != null) { + // skip import the client sessions, they should have been imported too. + log.debugf("The user-session already imported by another transaction for sessionId=%s offline=true", sessionId); + return existing; + } + + // we need to import the client sessions too. + log.debugf("Attempting to import the client-sessions for user-session with sessionId=%s offline=true", sessionId); + + var clientSessionsById = computeClientSessionsToImport(persistentUserSession, userSessionEntityToImport); + getClientSessionTransaction(true).importSessionsConcurrently(realm, clientSessionsById, offlineClientSessionCacheEntryLifespanAdjuster, SessionTimeouts::getOfflineClientSessionMaxIdleMs); + + return userSessionEntityToImport; + } + + private Map> computeClientSessionsToImport(UserSessionModel persistentUserSession, UserSessionEntity userSessionToImport) { + Map> clientSessionsById = new HashMap<>(); + AuthenticatedClientSessionStore clientSessions = userSessionToImport.getAuthenticatedClientSessions(); + int lastSessionRefresh = userSessionToImport.getLastSessionRefresh(); + String realmId = userSessionToImport.getRealmId(); + for (Map.Entry entry : persistentUserSession.getAuthenticatedClientSessions().entrySet()) { + String clientUUID = entry.getKey(); + AuthenticatedClientSessionModel clientSession = entry.getValue(); + AuthenticatedClientSessionEntity clientSessionToImport = createAuthenticatedClientSessionInstance(clientSession, + realmId, clientUUID, true); + + // Update timestamp to the same value as userSession. + // LastSessionRefresh of userSession from DB will have the correct value. + clientSessionToImport.setTimestamp(lastSessionRefresh); + + clientSessionsById.put(clientSessionToImport.getId(), new SessionEntityWrapper<>(clientSessionToImport)); + + // Update userSession entity with the clientSession + clientSessions.put(clientUUID, clientSessionToImport.getId()); + } + return clientSessionsById; } private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, boolean offline) { @@ -322,16 +342,6 @@ private UserSessionEntity getUserSessionEntity(RealmModel realm, String id, bool return entity; } - private Stream getUserSessionsFromPersistenceProviderStream(RealmModel realm, UserModel user, boolean offline) { - UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - return persister.loadUserSessionsStream(realm, user, offline, 0, null) - .map(persistentUserSession -> getUserSessionEntityFromCacheOrImportIfNecessary(realm, offline, persistentUserSession)) - .filter(Objects::nonNull) - .map(userSessionEntity -> (UserSessionModel) wrap(realm, userSessionEntity, offline)) - .filter(Objects::nonNull); - } - - protected Stream getUserSessionsStream(RealmModel realm, UserSessionPredicate predicate, boolean offline) { if (offline) { @@ -365,8 +375,8 @@ protected Stream getUserSessionsStream(RealmModel realm, UserS // return a stream that 'wraps' the infinispan cache stream so that the cache stream's elements are read one by one // and then mapped locally to avoid serialization issues when trying to manipulate the cache stream directly. - return StreamSupport.stream(getCache(offline).entrySet().stream().filter(predicate).map(Mappers.userSessionEntity()).spliterator(), false) - .map(entity -> this.wrap(realm, entity, offline)) + return StreamSupport.stream(getCache(false).entrySet().stream().filter(predicate).map(Mappers.userSessionEntity()).spliterator(), false) + .map(entity -> this.wrap(realm, entity, false)) .filter(Objects::nonNull).map(Function.identity()); } @@ -384,25 +394,25 @@ public AuthenticatedClientSessionAdapter getClientSession(UserSessionModel userS // offline client session lookup in the persister if (offline) { log.debugf("Offline client session is not found in cache, try to load from db, userSession [%s] clientSessionId [%s] clientId [%s]", userSession.getId(), clientSessionId, client.getClientId()); - return getClientSessionEntityFromPersistenceProvider(userSession, client, true); + return getClientSessionEntityFromPersistenceProvider(userSession, client); } return null; } - private AuthenticatedClientSessionAdapter getClientSessionEntityFromPersistenceProvider(UserSessionModel userSession, ClientModel client, boolean offline) { + private AuthenticatedClientSessionAdapter getClientSessionEntityFromPersistenceProvider(UserSessionModel userSession, ClientModel client) { UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); - AuthenticatedClientSessionModel clientSession = persister.loadClientSession(session.getContext().getRealm(), client, userSession, offline); + AuthenticatedClientSessionModel clientSession = persister.loadClientSession(session.getContext().getRealm(), client, userSession, true); if (clientSession == null) { return null; } - AuthenticatedClientSessionAdapter clientAdapter = importClientSession((UserSessionAdapter) userSession, clientSession, getTransaction(offline), - getClientSessionTransaction(offline), offline, true); + AuthenticatedClientSessionAdapter clientAdapter = importClientSession((UserSessionAdapter) userSession, clientSession, getTransaction(true), + getClientSessionTransaction(true), true); if (clientAdapter == null) { - persister.removeClientSession(userSession.getId(), client.getId(), offline); + persister.removeClientSession(userSession.getId(), client.getId(), true); } return clientAdapter; } @@ -445,9 +455,9 @@ protected Stream getUserSessionsStream(final RealmModel realm, // fetch the actual offline user session count from the database UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); return persister.loadUserSessionsStream(realm, client, true, firstResult, maxResults) - .map(persistentUserSession -> getUserSessionEntityFromCacheOrImportIfNecessary(realm, offline, persistentUserSession)) + .map(persistentUserSession -> getUserSessionEntityFromCacheOrImportIfNecessary(realm, persistentUserSession)) .filter(Objects::nonNull) - .map(userSessionEntity -> (UserSessionModel) wrap(realm, userSessionEntity, offline)) + .map(userSessionEntity -> (UserSessionModel) wrap(realm, userSessionEntity, true)) .filter(Objects::nonNull); } @@ -455,7 +465,7 @@ protected Stream getUserSessionsStream(final RealmModel realm, // If the sorted stream is used within a flatMap (like in SessionsResource), it will not terminate early unless wrapped with // StreamsUtil.prepareSortedStreamToWorkInsideOfFlatMapWithTerminalOperations causing unnecessary operations. - return paginatedStream(StreamsUtil.prepareSortedStreamToWorkInsideOfFlatMapWithTerminalOperations(getUserSessionsStream(realm, predicate, offline) + return paginatedStream(StreamsUtil.prepareSortedStreamToWorkInsideOfFlatMapWithTerminalOperations(getUserSessionsStream(realm, predicate, false) .sorted(Comparator.comparing(UserSessionModel::getLastSessionRefresh))), firstResult, maxResults); } @@ -489,7 +499,7 @@ public Map getActiveClientSessionStats(RealmModel realm, boolean o return persister.getUserSessionsCountsByClients(realm, true); } - return getCache(offline).entrySet().stream() + return getCache(false).entrySet().stream() .filter(UserSessionPredicate.create(realm.getId())) .map(Mappers.authClientSessionSetMapper()) .flatMap(CollectionToStreamMapper.getInstance()) @@ -504,7 +514,7 @@ protected long getUserSessionsCount(RealmModel realm, ClientModel client, boolea return persister.getUserSessionsCount(realm, client, true); } - return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).client(client.getId()), offline).count(); + return getUserSessionsStream(realm, UserSessionPredicate.create(realm.getId()).client(client.getId()), false).count(); } @Override @@ -535,7 +545,8 @@ protected void removeUserSessions(RealmModel realm, UserModel user, boolean offl public void removeAllExpired() { // Rely on expiration of cache entries provided by infinispan. Just expire entries from persister is needed // TODO: Avoid iteration over all realms here (Details in the KEYCLOAK-16802) - session.realms().getRealmsStream().forEach(this::removeExpired); + UserSessionPersisterProvider provider = session.getProvider(UserSessionPersisterProvider.class); + session.realms().getRealmsStream().forEach(provider::removeExpired); } @@ -562,10 +573,8 @@ protected void onRemoveUserSessionsEvent(String realmId) { public void removeLocalUserSessions(String realmId, boolean offline) { FuturesHelper futures = new FuturesHelper(); - Cache> cache = getCache(offline); - Cache> localCache = CacheDecorators.localCache(cache); - Cache> clientSessionCache = getClientSessionCache(offline); - Cache> localClientSessionCache = CacheDecorators.localCache(clientSessionCache); + Cache> localCache = CacheDecorators.localCache(getCache(offline)); + Cache> localClientSessionCache = CacheDecorators.localCache(getClientSessionCache(offline)); final AtomicInteger userSessionsSize = new AtomicInteger(); @@ -573,22 +582,16 @@ public void removeLocalUserSessions(String realmId, boolean offline) { .entrySet() .stream() .filter(SessionWrapperPredicate.create(realmId)) - .map(Mappers.userSessionEntity()) - .forEach(new Consumer() { - - @Override - public void accept(UserSessionEntity userSessionEntity) { - userSessionsSize.incrementAndGet(); - - // Remove session from remoteCache too. Use removeAsync for better perf - Future future = localCache.removeAsync(userSessionEntity.getId()); - futures.addTask(future); - userSessionEntity.getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { - Future f = localClientSessionCache.removeAsync(clientSessionId); - futures.addTask(f); - }); - } - + .forEach(userSessionEntity -> { + userSessionsSize.incrementAndGet(); + + // Remove session from remoteCache too. Use removeAsync for better perf + Future future = localCache.removeAsync(userSessionEntity.getKey()); + futures.addTask(future); + userSessionEntity.getValue().getEntity().getAuthenticatedClientSessions().forEach((clientUUID, clientSessionId) -> { + Future f = localClientSessionCache.removeAsync(clientSessionId); + futures.addTask(f); + }); }); @@ -617,9 +620,6 @@ protected void onRealmRemovedEvent(String realmId) { @Override public void onClientRemoved(RealmModel realm, ClientModel client) { -// clusterEventsSenderTx.addEvent( -// ClientRemovedSessionEvent.createEvent(ClientRemovedSessionEvent.class, InfinispanUserSessionProviderFactory.CLIENT_REMOVED_SESSION_EVENT, session, realm.getId(), true), -// ClusterProvider.DCNotify.LOCAL_DC_ONLY); UserSessionPersisterProvider sessionsPersister = session.getProvider(UserSessionPersisterProvider.class); if (sessionsPersister != null) { sessionsPersister.onClientRemoved(realm, client); @@ -655,7 +655,7 @@ protected void removeUserSession(UserSessionEntity sessionEntity, boolean offlin userSessionUpdateTx.addTask(sessionEntity.getId(), removeTask); } - UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline, UserModel user) { + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline, UserModel user) { InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(offline); @@ -663,14 +663,13 @@ UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offl return null; } - return new UserSessionAdapter(session, user, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline); + return new UserSessionAdapter<>(session, user, this, userSessionUpdateTx, clientSessionUpdateTx, realm, entity, offline); } - UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { - UserModel user = null; + UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offline) { if (Profile.isFeatureEnabled(Feature.TRANSIENT_USERS) && entity.getNotes().containsKey(SESSION_NOTE_LIGHTWEIGHT_USER)) { LightweightUserAdapter lua = LightweightUserAdapter.fromString(session, realm, entity.getNotes().get(SESSION_NOTE_LIGHTWEIGHT_USER)); - final UserSessionAdapter us = wrap(realm, entity, offline, lua); + final UserSessionAdapter us = wrap(realm, entity, offline, lua); lua.setUpdateHandler(lua1 -> { if (lua == lua1) { // Ensure there is no conflicting user model, only the latest lightweight user can be used us.setNote(SESSION_NOTE_LIGHTWEIGHT_USER, lua1.serialize()); @@ -679,7 +678,7 @@ UserSessionAdapter wrap(RealmModel realm, UserSessionEntity entity, boolean offl return us; } - user = session.users().getUserById(realm, entity.getUser()); + UserModel user = session.users().getUserById(realm, entity.getUser()); if (user == null) { // remove orphaned user session from the cache and from persister if the session is offline; also removes associated client sessions @@ -699,11 +698,11 @@ AuthenticatedClientSessionAdapter wrap(UserSessionModel userSession, ClientModel } UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSession, boolean offline) { - if (userSession instanceof UserSessionAdapter) { - if (!userSession.getRealm().equals(realm)) { + if (userSession instanceof UserSessionAdapter usa) { + if (!usa.getRealm().equals(realm)) { return null; } - return ((UserSessionAdapter) userSession).getEntity(); + return usa.getEntity(); } else { return getUserSessionEntity(realm, userSession.getId(), offline); } @@ -712,7 +711,7 @@ UserSessionEntity getUserSessionEntity(RealmModel realm, UserSessionModel userSe @Override public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { - UserSessionAdapter offlineUserSession = importUserSession(userSession, true); + UserSessionAdapter offlineUserSession = importUserSession(userSession); // started and lastSessionRefresh set to current time int currentTime = Time.currentTime(); @@ -725,7 +724,7 @@ public UserSessionModel createOfflineUserSession(UserSessionModel userSession) { } @Override - public UserSessionAdapter getOfflineUserSession(RealmModel realm, String userSessionId) { + public UserSessionAdapter getOfflineUserSession(RealmModel realm, String userSessionId) { return getUserSession(realm, userSessionId, true); } @@ -745,12 +744,13 @@ public void removeOfflineUserSession(RealmModel realm, UserSessionModel userSess @Override public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedClientSessionModel clientSession, UserSessionModel offlineUserSession) { - UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : + UserSessionAdapter userSessionAdapter = (offlineUserSession instanceof UserSessionAdapter) ? (UserSessionAdapter) offlineUserSession : getOfflineUserSession(offlineUserSession.getRealm(), offlineUserSession.getId()); InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); InfinispanChangelogBasedTransaction clientSessionUpdateTx = getClientSessionTransaction(true); - AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, true, false); + AuthenticatedClientSessionAdapter offlineClientSession = importClientSession(userSessionAdapter, clientSession, userSessionUpdateTx, clientSessionUpdateTx, false); + assert offlineClientSession != null; // no expiration checked, it is never null // update timestamp to current time offlineClientSession.setTimestamp(Time.currentTime()); @@ -764,7 +764,12 @@ public AuthenticatedClientSessionModel createOfflineClientSession(AuthenticatedC @Override public Stream getOfflineUserSessionsStream(RealmModel realm, UserModel user) { - return getUserSessionsFromPersistenceProviderStream(realm, user, true); + UserSessionPersisterProvider persister = session.getProvider(UserSessionPersisterProvider.class); + return persister.loadUserSessionsStream(realm, user, true, 0, null) + .map(persistentUserSession -> getUserSessionEntityFromCacheOrImportIfNecessary(realm, persistentUserSession)) + .filter(Objects::nonNull) + .map(userSessionEntity -> (UserSessionModel) wrap(realm, userSessionEntity, true)) + .filter(Objects::nonNull); } @Override @@ -778,7 +783,9 @@ public Stream getOfflineUserSessionsStream(RealmModel realm, C } + @SuppressWarnings("removal") @Override + @Deprecated(forRemoval = true, since = "25.0") public void importUserSessions(Collection persistentUserSessions, boolean offline) { if (persistentUserSessions == null || persistentUserSessions.isEmpty()) { return; @@ -821,9 +828,7 @@ public void importUserSessions(Collection persistentUserSessio offline ? offlineSessionCacheEntryLifespanAdjuster : SessionTimeouts::getUserSessionLifespanMs, offline ? SessionTimeouts::getOfflineSessionMaxIdleMs : SessionTimeouts::getUserSessionMaxIdleMs); } else { - Retry.executeWithBackoff((int iteration) -> { - cache.putAll(sessionsById); - }, 10, 10); + Retry.executeWithBackoff((int iteration) -> cache.putAll(sessionsById), 10, 10); } // Import client sessions @@ -834,14 +839,12 @@ public void importUserSessions(Collection persistentUserSessio offline ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs, offline ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs); } else { - Retry.executeWithBackoff((int iteration) -> { - clientSessCache.putAll(clientSessionsById); - }, 10, 10); + Retry.executeWithBackoff((int iteration) -> clientSessCache.putAll(clientSessionsById), 10, 10); } } - private void importSessionsWithExpiration(Map> sessionsById, - BasicCache cache, SessionFunction lifespanMsCalculator, + private void importSessionsWithExpiration(Map> sessionsById, + Cache> cache, SessionFunction lifespanMsCalculator, SessionFunction maxIdleTimeMsCalculator) { sessionsById.forEach((id, sessionEntityWrapper) -> { @@ -853,61 +856,37 @@ private void importSessionsWithExpiration(Map { - - try { - cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); - } catch (HotRodClientException re) { - if (log.isDebugEnabled()) { - log.debugf(re, "Failed to put import %d sessions to remoteCache. Iteration '%s'. Will try to retry the task", - sessionsById.size(), iteration); - } - - // Rethrow the exception. Retry will take care of handle the exception and eventually retry the operation. - throw re; - } - - }, 10, 10); - } else { - cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); - } + cache.put(id, sessionEntityWrapper, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); } }); } // Imports just userSession without it's clientSessions - protected UserSessionAdapter importUserSession(UserSessionModel userSession, boolean offline) { + protected UserSessionAdapter importUserSession(UserSessionModel userSession) { UserSessionEntity entity = UserSessionEntity.createFromModel(userSession); - InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(offline); + InfinispanChangelogBasedTransaction userSessionUpdateTx = getTransaction(true); SessionUpdateTask importTask = Tasks.addIfAbsentSync(); userSessionUpdateTx.addTask(userSession.getId(), importTask, entity, UserSessionModel.SessionPersistenceState.PERSISTENT); - UserSessionAdapter importedSession = wrap(userSession.getRealm(), entity, offline); - - return importedSession; + return wrap(userSession.getRealm(), entity, true); } - private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession, + private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter sessionToImportInto, AuthenticatedClientSessionModel clientSession, InfinispanChangelogBasedTransaction userSessionUpdateTx, InfinispanChangelogBasedTransaction clientSessionUpdateTx, - boolean offline, boolean checkExpiration) { + boolean checkExpiration) { AuthenticatedClientSessionEntity entity = createAuthenticatedClientSessionInstance(clientSession, - sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), offline); + sessionToImportInto.getRealm().getId(), clientSession.getClient().getId(), true); // Update timestamp to same value as userSession. LastSessionRefresh of userSession from DB will have correct value entity.setTimestamp(sessionToImportInto.getLastSessionRefresh()); if (checkExpiration) { - SessionFunction lifespanChecker = offline - ? offlineClientSessionCacheEntryLifespanAdjuster : SessionTimeouts::getClientSessionLifespanMs; - SessionFunction idleTimeoutChecker = offline - ? SessionTimeouts::getOfflineClientSessionMaxIdleMs : SessionTimeouts::getClientSessionMaxIdleMs; - if (idleTimeoutChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG - || lifespanChecker.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) { + if (SessionTimeouts.getOfflineClientSessionMaxIdleMs(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG + || offlineClientSessionCacheEntryLifespanAdjuster.apply(sessionToImportInto.getRealm(), clientSession.getClient(), entity) == SessionTimeouts.ENTRY_EXPIRED_FLAG) { return null; } } @@ -920,10 +899,9 @@ private AuthenticatedClientSessionAdapter importClientSession(UserSessionAdapter AuthenticatedClientSessionStore clientSessions = sessionToImportInto.getEntity().getAuthenticatedClientSessions(); clientSessions.put(clientSession.getClient().getId(), clientSessionId); - SessionUpdateTask registerClientSessionTask = new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId); - userSessionUpdateTx.addTask(sessionToImportInto.getId(), registerClientSessionTask); + userSessionUpdateTx.addTask(sessionToImportInto.getId(), new RegisterClientSessionTask(clientSession.getClient().getId(), clientSessionId)); - return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, offline); + return new AuthenticatedClientSessionAdapter(session, entity, clientSession.getClient(), sessionToImportInto, clientSessionUpdateTx, true); } @@ -944,15 +922,8 @@ private AuthenticatedClientSessionEntity createAuthenticatedClientSessionInstanc return entity; } - private static class RegisterClientSessionTask implements SessionUpdateTask { - - private final String clientUuid; - private final UUID clientSessionId; - - public RegisterClientSessionTask(String clientUuid, UUID clientSessionId) { - this.clientUuid = clientUuid; - this.clientSessionId = clientSessionId; - } + private record RegisterClientSessionTask(String clientUuid, UUID clientSessionId) + implements SessionUpdateTask { @Override public void runUpdate(UserSessionEntity session) { diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java index 12095a3a1278..0c1aee3a80b8 100755 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/InfinispanUserSessionProviderFactory.java @@ -148,7 +148,15 @@ public UserSessionProvider create(KeycloakSession session) { public void init(Config.Scope config) { this.config = config; offlineSessionCacheEntryLifespanOverride = config.getInt(CONFIG_OFFLINE_SESSION_CACHE_ENTRY_LIFESPAN_OVERRIDE, -1); + if (offlineSessionCacheEntryLifespanOverride != -1) { + // to be removed in KC 27 + log.warn("The option spi-user-sessions--infinispan--offline-session-cache-entry-lifespan-override is deprecated and will be removed in a future release"); + } offlineClientSessionCacheEntryLifespanOverride = config.getInt(CONFIG_OFFLINE_CLIENT_SESSION_CACHE_ENTRY_LIFESPAN_OVERRIDE, -1); + if (offlineClientSessionCacheEntryLifespanOverride != -1) { + // to be removed in KC 27 + log.warn("The option spi-user-sessions--infinispan--offline-client-session-cache-entry-lifespan-override is deprecated and will be removed in a future release"); + } maxBatchSize = config.getInt(CONFIG_MAX_BATCH_SIZE, DEFAULT_MAX_BATCH_SIZE); // Do not use caches for sessions if explicitly disabled or if embedded caches are not used useCaches = config.getBoolean(CONFIG_USE_CACHES, DEFAULT_USE_CACHES) && InfinispanUtils.isEmbeddedInfinispan(); @@ -340,13 +348,13 @@ public List getConfigMetadata() { builder.property() .name(CONFIG_OFFLINE_CLIENT_SESSION_CACHE_ENTRY_LIFESPAN_OVERRIDE) .type("int") - .helpText("Override how long offline client sessions should be kept in memory in seconds") + .helpText("Override how long offline client sessions should be kept in memory in seconds (deprecated, to be removed in Keycloak 27)") .add(); builder.property() .name(CONFIG_OFFLINE_SESSION_CACHE_ENTRY_LIFESPAN_OVERRIDE) .type("int") - .helpText("Override how long offline user sessions should be kept in memory in seconds") + .helpText("Override how long offline user sessions should be kept in memory in seconds (deprecated, to be removed in Keycloak 27)") .add(); builder.property() diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java index d2bcb64f78ae..3c3013b7714e 100644 --- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java +++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/changes/InfinispanChangelogBasedTransaction.java @@ -19,10 +19,13 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import org.infinispan.Cache; +import org.infinispan.commons.util.concurrent.CompletionStages; import org.jboss.logging.Logger; +import org.keycloak.connections.infinispan.InfinispanUtil; import org.keycloak.models.AbstractKeycloakTransaction; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -30,7 +33,7 @@ import org.keycloak.models.sessions.infinispan.CacheDecorators; import org.keycloak.models.sessions.infinispan.SessionFunction; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; -import org.keycloak.connections.infinispan.InfinispanUtil; +import org.keycloak.models.sessions.infinispan.util.SessionTimeouts; /** * @author Marek Posolda @@ -137,11 +140,9 @@ public SessionEntityWrapper get(K key) { return wrappedEntity; } else { // If entity is scheduled for remove, we don't return it. - boolean scheduledForRemove = myUpdates.getUpdateTasks().stream().filter((SessionUpdateTask task) -> { - - return task.getOperation() == SessionUpdateTask.CacheOperation.REMOVE; - - }).findFirst().isPresent(); + boolean scheduledForRemove = myUpdates.getUpdateTasks().stream() + .map(SessionUpdateTask::getOperation) + .anyMatch(SessionUpdateTask.CacheOperation.REMOVE::equals); return scheduledForRemove ? null : myUpdates.getEntityWrapper(); } @@ -258,7 +259,93 @@ private void replace(K key, MergedUpdate task, SessionEntityWrapper oldVer protected void rollbackImpl() { } - private SessionEntityWrapper generateNewVersionAndWrapEntity(V entity, Map localMetadata) { + /** + * @return The {@link Cache} backing up this transaction. + */ + public Cache> getCache() { + return cache; + } + + /** + * Imports a session from an external source into the {@link Cache}. + *

+ * If a session already exists in the cache, this method does not insert the {@code session}. The invoker should use + * the session returned by this method invocation. When the session is successfully imported, this method returns + * null and the {@code session} can be used by the transaction. + *

+ * This transaction will keep track of further changes in the session. + * + * @param realmModel The {@link RealmModel} where the session belong to. + * @param key The cache's key. + * @param session The session to import. + * @param lifespan How long the session stays cached until it is expired and removed. + * @param maxIdle How long the session can be idle (without reading or writing) before being removed. + * @return The existing cached session. If it returns {@code null}, it means the {@code session} used in the + * parameters was cached. + */ + public V importSession(RealmModel realmModel, K key, SessionEntityWrapper session, long lifespan, long maxIdle) { + SessionUpdatesList updatesList = updates.get(key); + if (updatesList != null) { + // exists in transaction, avoid cache operation + return updatesList.getEntityWrapper().getEntity(); + } + SessionEntityWrapper existing = cache.putIfAbsent(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); + if (existing == null) { + // keep track of the imported session for updates + updates.put(key, new SessionUpdatesList<>(realmModel, session)); + return null; + } + updates.put(key, new SessionUpdatesList<>(realmModel, existing)); + return existing.getEntity(); + } + + /** + * Imports multiple sessions from an external source into the {@link Cache}. + *

+ * If the {@code lifespanFunction} or {@code maxIdleFunction} returns {@link SessionTimeouts#ENTRY_EXPIRED_FLAG}, + * the session is considered expired and not stored in the cache. + *

+ * Also, if one or more sessions already exist in the {@link Cache}, it will not be imported. + *

+ * This transaction will keep track of further changes in the sessions. + * + * @param realmModel The {@link RealmModel} where the sessions belong to. + * @param sessions The {@link Map} with the cache's key/session mapping to be imported. + * @param lifespanFunction The {@link java.util.function.Function} to compute the lifespan of the session. It + * defines how long the session should be stored in the cache until it is removed. + * @param maxIdleFunction The {@link java.util.function.Function} to compute the max-idle of the session. It + * defines how long the session will be idle before it is removed. + */ + public void importSessionsConcurrently(RealmModel realmModel, Map> sessions, SessionFunction lifespanFunction, SessionFunction maxIdleFunction) { + if (sessions.isEmpty()) { + //nothing to import + return; + } + var stage = CompletionStages.aggregateCompletionStage(); + var allSessions = new ConcurrentHashMap>(); + sessions.forEach((key, session) -> { + if (updates.containsKey(key)) { + //nothing to import, already exists in transaction + return; + } + var clientModel = session.getClientIfNeeded(realmModel); + var sessionEntity = session.getEntity(); + var lifespan = lifespanFunction.apply(realmModel, clientModel, sessionEntity); + var maxIdle = maxIdleFunction.apply(realmModel, clientModel, sessionEntity); + if (lifespan == SessionTimeouts.ENTRY_EXPIRED_FLAG || maxIdle == SessionTimeouts.ENTRY_EXPIRED_FLAG) { + //nothing to import, already expired + return; + } + var future = cache.putIfAbsentAsync(key, session, lifespan, TimeUnit.MILLISECONDS, maxIdle, TimeUnit.MILLISECONDS); + // write result into concurrent hash map because the consumer is invoked in a different thread each time. + stage.dependsOn(future.thenAccept(existing -> allSessions.put(key, existing == null ? session : existing))); + }); + + CompletionStages.join(stage.freeze()); + allSessions.forEach((key, wrapper) -> updates.put(key, new SessionUpdatesList<>(realmModel, wrapper))); + } + + private static SessionEntityWrapper generateNewVersionAndWrapEntity(V entity, Map localMetadata) { return new SessionEntityWrapper<>(localMetadata, entity); } diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java index 311dc96ae29c..f65eb848d252 100644 --- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java @@ -216,7 +216,7 @@ public static void configureSessionsCachesForPersistentSessions(ConfigurationBui */ public static void configureSessionsCachesForVolatileSessions(ConfigurationBuilderHolder holder) { logger.debug("Configuring session cache (volatile user sessions)"); - for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) { + for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME)) { var builder = holder.getNamedConfigurationBuilders().get(name); if (builder == null) { throw cacheNotFound(name); @@ -232,6 +232,23 @@ public static void configureSessionsCachesForVolatileSessions(ConfigurationBuild builder.clustering().hash().numOwners(2); } } + + for (var name : Arrays.asList( OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) { + var builder = holder.getNamedConfigurationBuilders().get(name); + if (builder == null) { + throw cacheNotFound(name); + } + if (builder.memory().maxCount() == -1) { + logger.infof("Offline sessions should have a max count set to avoid excessive memory usage. Setting a default cache limit of 10000 for cache %s.", name); + builder.memory().maxCount(10000); + } + if (builder.clustering().hash().attributes().attribute(HashConfiguration.NUM_OWNERS).get() != 1 && + builder.persistence().stores().stream().noneMatch(p -> p.attributes().attribute(AbstractStoreConfiguration.SHARED).get()) + ) { + logger.infof("Setting a memory limit implies to have exactly one owne. Setting num_owners=1 to avoid data loss.", name); + builder.clustering().hash().numOwners(1); + } + } } // private methods below diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java index c36092355556..26e846b7628a 100644 --- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java @@ -54,6 +54,7 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ALL_CACHES_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_MAX_COUNT_CACHES; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_CACHE_NAMES; +import static org.keycloak.spi.infinispan.impl.embedded.JGroupsConfigurator.createJGroupsProperties; /** * The default implementation of {@link CacheEmbeddedConfigProviderFactory}. @@ -121,6 +122,7 @@ public List getConfigMetadata() { Stream.concat(Arrays.stream(LOCAL_CACHE_NAMES), Arrays.stream(CLUSTERED_MAX_COUNT_CACHES)) .forEach(name -> Util.copyFromOption(builder, CacheConfigurator.maxCountConfigKey(name), "max-count", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.maxCountOption(name), false)); createTopologyProperties(builder); + createJGroupsProperties(builder); return builder.build(); } diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java index 1653cdbc468e..3af902bfc971 100644 --- a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java @@ -17,20 +17,31 @@ package org.keycloak.spi.infinispan.impl.embedded; +import static org.infinispan.configuration.global.TransportConfiguration.STACK; +import static org.keycloak.config.CachingOptions.CACHE_EMBEDDED_PREFIX; + import java.lang.invoke.MethodHandles; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.TrustManager; + import org.infinispan.commons.configuration.attributes.Attribute; import org.infinispan.configuration.global.TransportConfigurationBuilder; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; import org.infinispan.remoting.transport.jgroups.JGroupsTransport; import org.jboss.logging.Logger; +import org.jgroups.Global; import org.jgroups.conf.ClassConfigurator; import org.jgroups.conf.ProtocolConfiguration; import org.jgroups.protocols.TCP; @@ -40,6 +51,8 @@ import org.jgroups.util.DefaultSocketFactory; import org.jgroups.util.SocketFactory; import org.keycloak.Config; +import org.keycloak.config.CachingOptions; +import org.keycloak.config.Option; import org.keycloak.connections.infinispan.InfinispanConnectionProvider; import org.keycloak.connections.infinispan.InfinispanConnectionSpi; import org.keycloak.connections.jpa.JpaConnectionProvider; @@ -48,15 +61,10 @@ import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2; import org.keycloak.models.KeycloakSession; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; import org.keycloak.spi.infinispan.JGroupsCertificateProvider; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.TrustManager; - -import static org.infinispan.configuration.global.TransportConfiguration.STACK; +import org.keycloak.spi.infinispan.impl.Util; /** * Utility class to configure JGroups based on the Keycloak configuration. @@ -88,6 +96,7 @@ public static void configureJGroups(Config.Scope config, ConfigurationBuilderHol if (stack != null) { transportOf(holder).stack(stack); } + configureTransport(config); configureDiscovery(holder, session); configureTls(holder, session); warnDeprecatedStack(holder); @@ -126,6 +135,17 @@ public static void configureTopology(Config.Scope config, ConfigurationBuilderHo } } + static void createJGroupsProperties(ProviderConfigurationBuilder builder) { + Util.copyFromOption(builder, SystemProperties.BIND_ADDRESS.configKey, "address", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_ADDRESS, false); + Util.copyFromOption(builder, SystemProperties.BIND_PORT.configKey, "port", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT, false); + Util.copyFromOption(builder, SystemProperties.EXTERNAL_ADDRESS.configKey, "address", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS, false); + Util.copyFromOption(builder, SystemProperties.EXTERNAL_PORT.configKey, "port", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT, false); + } + + private static void configureTransport(Config.Scope config) { + Arrays.stream(SystemProperties.values()).forEach(p -> p.set(config)); + } + private static void configureTls(ConfigurationBuilderHolder holder, KeycloakSession session) { var provider = session.getProvider(JGroupsCertificateProvider.class); if (provider == null || !provider.isEnabled()) { @@ -269,4 +289,69 @@ public void afterCreation(Protocol protocol) { } } } + + private enum SystemProperties { + BIND_ADDRESS(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_ADDRESS, Global.BIND_ADDR, "jgroups.bind.address"), + BIND_PORT(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT, Global.BIND_PORT, "jgroups.bind.port"), + EXTERNAL_ADDRESS(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS, Global.EXTERNAL_ADDR), + EXTERNAL_PORT(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT, Global.EXTERNAL_PORT); + + final Option option; + final String property; + final String altProperty; + final String configKey; + + SystemProperties(Option option, String property) { + this(option, property, null); + } + + SystemProperties(Option option, String property, String altProperty) { + this.option = option; + this.property = property; + this.altProperty = altProperty; + this.configKey = configKey(); + } + + void set(Config.Scope config) { + String userConfig = fromConfig(config); + if (userConfig == null) { + // User property is either already set or missing, so do nothing + return; + } + checkPropertyAlreadySet(userConfig, property); + if (altProperty != null) + checkPropertyAlreadySet(userConfig, altProperty); + System.setProperty(property, userConfig); + } + + void checkPropertyAlreadySet(String userValue, String property) { + String userProp = System.getProperty(property); + if (userProp != null) { + logger.warnf("Conflicting system property '%s' and CLI arg '%s' set, utilising CLI value '%s'", + property, option.getKey(), userValue); + System.clearProperty(property); + } + } + + String fromConfig(Config.Scope config) { + if (option.getType() == Integer.class) { + Integer val = config.getInt(configKey); + return val == null ? null : val.toString(); + } + return config.get(configKey); + } + + String configKey() { + // Strip the scope from the key and convert to camelCase + String key = option.getKey().substring(CACHE_EMBEDDED_PREFIX.length() + 1); + StringBuilder sb = new StringBuilder(key); + for (int i = 0; i < sb.length(); i++) { + if (sb.charAt(i) == '-') { + sb.deleteCharAt(i); + sb.replace(i, i+1, String.valueOf(Character.toUpperCase(sb.charAt(i)))); + } + } + return sb.toString(); + } + } } diff --git a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java index 17a29b9dd7a2..4fe167511f49 100644 --- a/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java +++ b/operator/src/main/java/org/keycloak/operator/controllers/KeycloakDeploymentDependentResource.java @@ -151,14 +151,18 @@ public StatefulSet desired(Keycloak primary, Context context) { var existingDeployment = ContextUtils.getCurrentStatefulSet(context).orElse(null); + String serviceName = KeycloakDiscoveryServiceDependentResource.getName(primary); if (existingDeployment != null) { // copy the existing annotations to keep the status consistent CRDUtils.findUpdateReason(existingDeployment).ifPresent(r -> baseDeployment.getMetadata().getAnnotations() .put(Constants.KEYCLOAK_UPDATE_REASON_ANNOTATION, r)); CRDUtils.fetchIsRecreateUpdate(existingDeployment).ifPresent(b -> baseDeployment.getMetadata() .getAnnotations().put(Constants.KEYCLOAK_RECREATE_UPDATE_ANNOTATION, b.toString())); + serviceName = existingDeployment.getSpec().getServiceName(); } + baseDeployment.getSpec().setServiceName(serviceName); + var updateType = ContextUtils.getUpdateType(context); if (existingDeployment == null || updateType.isEmpty()) { @@ -287,7 +291,6 @@ private StatefulSet createBaseDeployment(Keycloak keycloakCR, Context .editOrNewSpec().withImagePullSecrets(keycloakCR.getSpec().getImagePullSecrets()).endSpec() .endTemplate() .withReplicas(keycloakCR.getSpec().getInstances()) - .withServiceName(KeycloakDiscoveryServiceDependentResource.getName(keycloakCR)) .endSpec(); var specBuilder = baseDeploymentBuilder.editSpec().editTemplate().editOrNewSpec(); diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java index 453d3353e516..d65837b96cf9 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/KeycloakDeploymentTest.java @@ -47,7 +47,6 @@ import org.keycloak.operator.crds.v2alpha1.deployment.ValueOrSecret; import org.keycloak.operator.crds.v2alpha1.deployment.spec.BootstrapAdminSpec; import org.keycloak.operator.crds.v2alpha1.deployment.spec.HostnameSpecBuilder; -import org.keycloak.operator.crds.v2alpha1.deployment.spec.ProbeSpec; import org.keycloak.operator.testsuite.apiserver.DisabledIfApiServerTest; import org.keycloak.operator.testsuite.unit.WatchedResourcesTest; import org.keycloak.operator.testsuite.utils.CRAssert; @@ -213,6 +212,7 @@ public void testConfigInCRTakesPrecedence() { @Test public void testDeploymentDurability() { var kc = getTestKeycloakDeployment(true); + KeycloakDeploymentTest.initCustomBootstrapAdminUser(kc); var deploymentName = kc.getMetadata().getName(); // create a dummy StatefulSet representing the pre-multiinstance state that we'll be forced to delete @@ -395,14 +395,19 @@ public void testInitialAdminUser() { @Test public void testCustomBootstrapAdminUser() { var kc = getTestKeycloakDeployment(true); + String secretName = initCustomBootstrapAdminUser(kc); + assertInitialAdminUser(secretName, kc, true); + } + + static String initCustomBootstrapAdminUser(Keycloak kc) { String secretName = "my-secret"; // fluents don't seem to work here because of the inner classes kc.getSpec().setBootstrapAdminSpec(new BootstrapAdminSpec()); kc.getSpec().getBootstrapAdminSpec().setUser(new BootstrapAdminSpec.User()); kc.getSpec().getBootstrapAdminSpec().getUser().setSecret(secretName); k8sclient.resource(new SecretBuilder().withNewMetadata().withName(secretName).endMetadata() - .addToStringData("username", "user").addToStringData("password", "pass20rd").build()).create(); - assertInitialAdminUser(secretName, kc, true); + .addToStringData("username", "user").addToStringData("password", "pass20rd").build()).serverSideApply(); + return secretName; } // Reference curl command: diff --git a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java index 4d7d0c1e5756..4d6517b4c364 100644 --- a/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java +++ b/operator/src/test/java/org/keycloak/operator/testsuite/integration/PodTemplateTest.java @@ -22,6 +22,8 @@ import io.fabric8.kubernetes.api.model.NamespaceBuilder; import io.fabric8.kubernetes.api.model.PodTemplateSpecBuilder; import io.fabric8.kubernetes.api.model.Secret; +import io.fabric8.kubernetes.api.model.apps.StatefulSet; +import io.fabric8.kubernetes.api.model.apps.StatefulSetBuilder; import io.fabric8.kubernetes.client.dsl.Resource; import io.fabric8.kubernetes.client.utils.Serialization; import io.quarkus.logging.Log; @@ -29,16 +31,20 @@ import org.awaitility.Awaitility; import org.junit.jupiter.api.Test; +import org.keycloak.operator.Utils; import org.keycloak.operator.crds.v2alpha1.deployment.Keycloak; import org.keycloak.operator.testsuite.apiserver.DisabledIfApiServerTest; import org.keycloak.operator.testsuite.utils.CRAssert; import org.keycloak.operator.testsuite.utils.K8sUtils; import java.util.Collections; +import java.util.concurrent.TimeUnit; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.keycloak.operator.crds.v2alpha1.deployment.KeycloakStatusCondition.HAS_ERRORS; +import static org.keycloak.operator.testsuite.utils.K8sUtils.deployKeycloak; import static org.keycloak.operator.testsuite.utils.K8sUtils.getResourceFromFile; @QuarkusTest @@ -229,4 +235,31 @@ public void testPodTemplateIncorrectImagePullSecretsConfig() { }); } + @Test + public void testDeploymentUpgrade() { + var kc = getTestKeycloakDeployment(true); + kc.getSpec().setInstances(2); + // all preconditions must be met, otherwise the operator sdk will remove the existing statefulset + KeycloakDeploymentTest.initCustomBootstrapAdminUser(kc); + + // create a dummy StatefulSet representing the 26.0 state that we'll be forced to delete + StatefulSet statefulSet = new StatefulSetBuilder().withMetadata(kc.getMetadata()).editMetadata() + .addToLabels(Utils.allInstanceLabels(kc)).endMetadata().withNewSpec().withNewSelector() + .withMatchLabels(Utils.allInstanceLabels(kc)).endSelector().withReplicas(0) + .withNewTemplate().withNewMetadata().withLabels(Utils.allInstanceLabels(kc)).endMetadata() + .withNewSpec().addNewContainer().withName("pause").withImage("registry.k8s.io/pause:3.1") + .endContainer().endSpec().endTemplate().endSpec().build(); + var ss = k8sclient.resource(statefulSet).create(); + + // start will not be successful because the statefulSet is in the way + deployKeycloak(k8sclient, kc, false); + // once the statefulset is owned by the keycloak it will be picked up by the informer + k8sclient.resource(statefulSet).accept(s -> s.addOwnerReference(k8sclient.resource(kc).get())); + + Awaitility.await().atMost(1, TimeUnit.MINUTES).until(() -> k8sclient.resource(statefulSet).get().getSpec().getReplicas() == 2); + + // we don't expect a recreate - that would indicate the operator sdk saw a precondition failing + assertEquals(ss.getMetadata().getUid(), k8sclient.resource(statefulSet).get().getMetadata().getUid()); + } + } diff --git a/pom.xml b/pom.xml index 1ffa108e02a0..5854c056b60a 100644 --- a/pom.xml +++ b/pom.xml @@ -90,8 +90,8 @@ 2.3.230 6.2.13.Final 6.2.13.Final - 15.0.15.Final - 5.0.13.Final + 15.0.16.Final + 5.0.14.Final ${protostream.version} diff --git a/quarkus/CONTRIBUTING.md b/quarkus/CONTRIBUTING.md index 7bf6aa66cd1e..37666b49fb2c 100644 --- a/quarkus/CONTRIBUTING.md +++ b/quarkus/CONTRIBUTING.md @@ -104,7 +104,7 @@ The `kc.sh|bat` script allows you to remotely debug the distribution. For that, kc.sh --debug start-dev ``` -By default, the debug port is available at `8787`. +By default, the debug port is available at `8787`. Additionally, you can specify IPv4 or bracketed IPv6 addresses with optional ports, e.g. `--debug 127.0.0.1`, `--debug 127.0.0.1:8786`, `--debug [::1]`, `--debug [::1]:8785`. Make sure to exercise caution when setting IP addresses in the `--debug` parameter, since a value such as `--debug 0.0.0.0:8787` will expose the debug port to all network interfaces! An additional environment variable `DEBUG_SUSPEND` can be set to suspend the JVM, when launched in debug mode. The `DEBUG_SUSPEND` variable supports the following values: diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java index 0e6a611b0491..439b1b3fffdf 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/CachingOptions.java @@ -8,7 +8,7 @@ public class CachingOptions { public static final String CACHE_CONFIG_FILE_PROPERTY = "cache-config-file"; - private static final String CACHE_EMBEDDED_PREFIX = "cache-embedded"; + public static final String CACHE_EMBEDDED_PREFIX = "cache-embedded"; private static final String CACHE_EMBEDDED_MTLS_PREFIX = CACHE_EMBEDDED_PREFIX + "-mtls"; public static final String CACHE_EMBEDDED_MTLS_ENABLED_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-enabled"; public static final String CACHE_EMBEDDED_MTLS_KEYSTORE_FILE_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-key-store-file"; @@ -16,6 +16,10 @@ public class CachingOptions { public static final String CACHE_EMBEDDED_MTLS_TRUSTSTORE_FILE_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-trust-store-file"; public static final String CACHE_EMBEDDED_MTLS_TRUSTSTORE_PASSWORD_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-trust-store-password"; public static final String CACHE_EMBEDDED_MTLS_ROTATION_PROPERTY = CACHE_EMBEDDED_MTLS_PREFIX + "-rotation-interval-days"; + public static final String CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-bind-address"; + public static final String CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-bind-port"; + public static final String CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-external-address"; + public static final String CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT_PROPERTY = CACHE_EMBEDDED_PREFIX + "-network-external-port"; private static final String CACHE_REMOTE_PREFIX = "cache-remote"; public static final String CACHE_REMOTE_HOST_PROPERTY = CACHE_REMOTE_PREFIX + "-host"; @@ -109,6 +113,28 @@ public String toString() { .description("Rotation period in days of automatic JGroups MTLS certificates.") .build(); + public static final Option CACHE_EMBEDDED_NETWORK_BIND_ADDRESS = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY, String.class) + .category(OptionCategory.CACHE) + .description("IP address used by clustering transport. By default, SITE_LOCAL is used.") + .build(); + + public static final Option CACHE_EMBEDDED_NETWORK_BIND_PORT = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY, Integer.class) + .category(OptionCategory.CACHE) + .description("The Port the clustering transport will bind to. By default, port 7800 is used.") + .build(); + + public static final Option CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS_PROPERTY, String.class) + .category(OptionCategory.CACHE) + .description("IP address that other instances in the cluster should use to contact this node. Set only if it is " + + "different to %s, for example when this instance is behind a firewall.".formatted(CACHE_EMBEDDED_NETWORK_BIND_ADDRESS_PROPERTY)) + .build(); + + public static final Option CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT = new OptionBuilder<>(CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT_PROPERTY, Integer.class) + .category(OptionCategory.CACHE) + .description("Port that other instances in the cluster should use to contact this node. Set only if it is different " + + "to %s, for example when this instance is behind a firewall".formatted(CACHE_EMBEDDED_NETWORK_BIND_PORT_PROPERTY)) + .build(); + public static final Option CACHE_REMOTE_HOST = new OptionBuilder<>(CACHE_REMOTE_HOST_PROPERTY, String.class) .category(OptionCategory.CACHE) .description("The hostname of the external Infinispan cluster.") diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java index 183fed3b98fc..bd719e018b25 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java @@ -106,6 +106,11 @@ public class DatabaseOptions { .description("Deactivate specific named datasource .") .build(); + public static final Option DB_POSTGRESQL_TARGET_SERVER_TYPE = new OptionBuilder<>("db-postgres-target-server-type", String.class) + .category(OptionCategory.DATABASE) + .hidden() + .build(); + /** * Options that have their sibling for a named datasource * Example: for `db-dialect`, `db-dialect-` is created diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java index 9564ce73260e..bf6038c39607 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/OptionCategory.java @@ -4,7 +4,7 @@ public enum OptionCategory { CACHE("Cache", 10, ConfigSupportLevel.SUPPORTED), CONFIG("Config", 15, ConfigSupportLevel.SUPPORTED), DATABASE("Database", 20, ConfigSupportLevel.SUPPORTED), - DATABASE_DATASOURCES("Database - additional datasources", 21, ConfigSupportLevel.SUPPORTED), + DATABASE_DATASOURCES("Database - additional datasources", 21, ConfigSupportLevel.PREVIEW), TRANSACTION("Transaction",30, ConfigSupportLevel.SUPPORTED), FEATURE("Feature", 40, ConfigSupportLevel.SUPPORTED), HOSTNAME_V2("Hostname v2", 50, ConfigSupportLevel.SUPPORTED), diff --git a/quarkus/dist/src/main/content/bin/kc.bat b/quarkus/dist/src/main/content/bin/kc.bat index 437f34e8ceb7..c808a0021a46 100644 --- a/quarkus/dist/src/main/content/bin/kc.bat +++ b/quarkus/dist/src/main/content/bin/kc.bat @@ -8,21 +8,22 @@ setlocal rem Get the program name before using shift as the command modify the variable ~nx0 if "%OS%" == "Windows_NT" ( - set PROGNAME=%~nx0% + set PROGNAME=%~nx0% ) else ( - set PROGNAME=kc.bat + set PROGNAME=kc.bat ) if "%OS%" == "Windows_NT" ( - set "DIRNAME=%~dp0" + set "DIRNAME=%~dp0" ) else ( - set DIRNAME=.\ + set DIRNAME=.\ ) set SERVER_OPTS=-Djava.util.logging.manager=org.jboss.logmanager.LogManager -Dquarkus-log-max-startup-records=10000 -Dpicocli.disable.closures=true set DEBUG_MODE=false set DEBUG_PORT_VAR=8787 +set DEBUG_ADDRESS=0.0.0.0:%DEBUG_PORT_VAR% set DEBUG_SUSPEND_VAR=n set CONFIG_ARGS= @@ -33,20 +34,29 @@ if "%KEY%" == "" ( goto MAIN ) if "%KEY%" == "--debug" ( - set DEBUG_MODE=true - if 1%2 EQU +1%2 ( - set DEBUG_PORT_VAR=%2 - shift - ) else ( - set DEBUG_PORT_VAR=8787 - ) - shift - goto READ-ARGS + set DEBUG_MODE=true + if 1%2 EQU +1%2 ( + rem Plain port + set DEBUG_ADDRESS=0.0.0.0:%2 + shift + ) else ( + rem IPv4 or IPv6 address with optional port + (echo %2 | findstr /R "[0-9].*\." >nul || echo %2 | findstr /R "\[.*:.*\]" >nul) && ( + (echo %2 | findstr /R "]:[0-9][0-9]*" >nul || echo %2 | findstr /R "^[0-9].*:[0-9][0-9]*" >nul) && ( + set DEBUG_ADDRESS=%2 + ) || ( + set DEBUG_ADDRESS=%2:%DEBUG_PORT_VAR% + ) + shift + ) + ) + shift + goto READ-ARGS ) if "%KEY%" == "start-dev" ( - set CONFIG_ARGS=%CONFIG_ARGS% --profile=dev %KEY% - shift - goto READ-ARGS + set CONFIG_ARGS=%CONFIG_ARGS% --profile=dev %KEY% + shift + goto READ-ARGS ) set "VALUE=%~2" set PROBABLY_VALUE=false @@ -58,25 +68,25 @@ if "%VALUE%" NEQ "" ( ) ) if "%KEY:~0,2%"=="-D" ( - if %PROBABLY_VALUE%==true ( - set SERVER_OPTS=%SERVER_OPTS% %KEY%^=%VALUE% + if %PROBABLY_VALUE%==true ( + set SERVER_OPTS=%SERVER_OPTS% %KEY%^=%VALUE% + shift + ) else ( + set SERVER_OPTS=%SERVER_OPTS% %KEY% + ) shift - ) else ( - set SERVER_OPTS=%SERVER_OPTS% %KEY% - ) - shift - goto READ-ARGS + goto READ-ARGS ) if not "%KEY:~0,1%"=="-" ( - set CONFIG_ARGS=%CONFIG_ARGS% %1 - shift - goto READ-ARGS + set CONFIG_ARGS=%CONFIG_ARGS% %1 + shift + goto READ-ARGS ) if %PROBABLY_VALUE%==true ( - set CONFIG_ARGS=%CONFIG_ARGS% %1 %2 - shift + set CONFIG_ARGS=%CONFIG_ARGS% %1 %2 + shift ) else ( - set CONFIG_ARGS=%CONFIG_ARGS% %1 + set CONFIG_ARGS=%CONFIG_ARGS% %1 ) shift goto READ-ARGS @@ -86,91 +96,90 @@ goto READ-ARGS setlocal EnableDelayedExpansion if not "x%JAVA_OPTS%" == "x" ( - echo "JAVA_OPTS already set in environment; overriding default settings" + echo "JAVA_OPTS already set in environment; overriding default settings" ) else ( - rem The defaults set up Keycloak with '-XX:+UseG1GC -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=70 -XX:GCTimeRatio=12 -XX:AdaptiveSizePolicyWeight=10' which proved to provide a good throughput and efficiency in the total memory allocation and CPU overhead. - rem If the memory is not used, it will be freed. See https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers for details. - rem To optimize for large heap sizes or for throughput and better response time due to shorter GC pauses, consider ZGC and Shenandoah GC. - rem As of KC25 and JDK17, G1GC, ZGC and Shenandoah GC seem to be eager to claim the maximum heap size. Tests showed that ZGC might need additional tuning in reclaiming dead objects. - set "JAVA_OPTS=-XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseG1GC -XX:FlightRecorderOptions=stackdepth=512" - - if "x%JAVA_OPTS_KC_HEAP%" == "x" ( - if "!KC_RUN_IN_CONTAINER!" == "true" ( - rem Maximum utilization of the heap is set to 70% of the total container memory - rem Initial heap size is set to 50% of the total container memory in order to reduce GC executions - set "JAVA_OPTS_KC_HEAP=-XX:MaxRAMPercentage=70 -XX:MinRAMPercentage=70 -XX:InitialRAMPercentage=50" + rem The defaults set up Keycloak with '-XX:+UseG1GC -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=70 -XX:GCTimeRatio=12 -XX:AdaptiveSizePolicyWeight=10' which proved to provide a good throughput and efficiency in the total memory allocation and CPU overhead. + rem If the memory is not used, it will be freed. See https://developers.redhat.com/blog/2017/04/04/openjdk-and-containers for details. + rem To optimize for large heap sizes or for throughput and better response time due to shorter GC pauses, consider ZGC and Shenandoah GC. + rem As of KC25 and JDK17, G1GC, ZGC and Shenandoah GC seem to be eager to claim the maximum heap size. Tests showed that ZGC might need additional tuning in reclaiming dead objects. + set "JAVA_OPTS=-XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.err.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Djava.security.egd=file:/dev/urandom -XX:+UseG1GC -XX:FlightRecorderOptions=stackdepth=512" + + if "x%JAVA_OPTS_KC_HEAP%" == "x" ( + if "!KC_RUN_IN_CONTAINER!" == "true" ( + rem Maximum utilization of the heap is set to 70% of the total container memory + rem Initial heap size is set to 50% of the total container memory in order to reduce GC executions + set "JAVA_OPTS_KC_HEAP=-XX:MaxRAMPercentage=70 -XX:MinRAMPercentage=70 -XX:InitialRAMPercentage=50" + ) else ( + set "JAVA_OPTS_KC_HEAP=-Xms64m -Xmx512m" + ) ) else ( - set "JAVA_OPTS_KC_HEAP=-Xms64m -Xmx512m" + echo "JAVA_OPTS_KC_HEAP already set in environment; overriding default settings" ) - ) else ( - echo "JAVA_OPTS_KC_HEAP already set in environment; overriding default settings" - ) - - set "JAVA_OPTS=!JAVA_OPTS! !JAVA_OPTS_KC_HEAP!" + set "JAVA_OPTS=!JAVA_OPTS! !JAVA_OPTS_KC_HEAP!" ) @REM See also https://github.com/wildfly/wildfly-core/blob/7e5624cf92ebe4b64a4793a8c0b2a340c0d6d363/core-feature-pack/common/src/main/resources/content/bin/common.sh#L57-L60 if not "x%JAVA_ADD_OPENS%" == "x" ( - echo "JAVA_ADD_OPENS already set in environment; overriding default settings" + echo "JAVA_ADD_OPENS already set in environment; overriding default settings" ) else ( - set "JAVA_ADD_OPENS=--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED" + set "JAVA_ADD_OPENS=--add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED" ) set "JAVA_OPTS=%JAVA_OPTS% %JAVA_ADD_OPENS%" @REM Set the default locale for the JVM to English to prevent locale-specific character variations if not "x%JAVA_LOCALE%" == "x" ( - echo "JAVA_LOCALE already set in environment; overriding default settings" + echo "JAVA_LOCALE already set in environment; overriding default settings" ) else ( - set "JAVA_LOCALE=-Duser.language=en -Duser.country=US" + set "JAVA_LOCALE=-Duser.language=en -Duser.country=US" ) set "JAVA_OPTS=%JAVA_OPTS% %JAVA_LOCALE%" if not "x%JAVA_OPTS_APPEND%" == "x" ( - echo "Appending additional Java properties to JAVA_OPTS" - set JAVA_OPTS=%JAVA_OPTS% %JAVA_OPTS_APPEND% + echo "Appending additional Java properties to JAVA_OPTS" + set JAVA_OPTS=%JAVA_OPTS% %JAVA_OPTS_APPEND% ) if NOT "x%DEBUG%" == "x" ( - set DEBUG_MODE=%DEBUG% + set DEBUG_MODE=%DEBUG% ) if NOT "x%DEBUG_PORT%" == "x" ( - set DEBUG_PORT_VAR=%DEBUG_PORT% + set DEBUG_PORT_VAR=%DEBUG_PORT% ) if NOT "x%DEBUG_SUSPEND%" == "x" ( - set DEBUG_SUSPEND_VAR=%DEBUG_SUSPEND% + set DEBUG_SUSPEND_VAR=%DEBUG_SUSPEND% ) rem Set debug settings if not already set if "%DEBUG_MODE%" == "true" ( - echo "%JAVA_OPTS%" | findstr /I "\-agentlib:jdwp" > nul - if errorlevel == 1 ( - set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=%DEBUG_PORT_VAR%,server=y,suspend=%DEBUG_SUSPEND_VAR% - ) else ( - echo Debug already enabled in JAVA_OPTS, ignoring --debug argument - ) + echo "%JAVA_OPTS%" | findstr /I "\-agentlib:jdwp" > nul + if errorlevel == 1 ( + set JAVA_OPTS=%JAVA_OPTS% -agentlib:jdwp=transport=dt_socket,address=%DEBUG_ADDRESS%,server=y,suspend=%DEBUG_SUSPEND_VAR% + ) else ( + echo Debug already enabled in JAVA_OPTS, ignoring --debug argument + ) ) rem Setup Keycloak specific properties set JAVA_OPTS=-Dprogram.name=%PROGNAME% %JAVA_OPTS% if "x%JAVA%" == "x" ( - if "x%JAVA_HOME%" == "x" ( - set JAVA=java - echo JAVA_HOME is not set. Unexpected results may occur. 1>&2 - echo Set JAVA_HOME to the directory of your local JDK to avoid this message. 1>&2 - ) else ( - if not exist "%JAVA_HOME%" ( - echo JAVA_HOME "%JAVA_HOME%" path doesn't exist 1>&2 - goto END + if "x%JAVA_HOME%" == "x" ( + set JAVA=java + echo JAVA_HOME is not set. Unexpected results may occur. 1>&2 + echo Set JAVA_HOME to the directory of your local JDK to avoid this message. 1>&2 ) else ( - if not exist "%JAVA_HOME%\bin\java.exe" ( - echo "%JAVA_HOME%\bin\java.exe" does not exist 1>&2 + if not exist "%JAVA_HOME%" ( + echo JAVA_HOME "%JAVA_HOME%" path doesn't exist 1>&2 goto END - ) - set "JAVA=%JAVA_HOME%\bin\java" + ) else ( + if not exist "%JAVA_HOME%\bin\java.exe" ( + echo "%JAVA_HOME%\bin\java.exe" does not exist 1>&2 + goto END + ) + set "JAVA=%JAVA_HOME%\bin\java" ) ) ) @@ -196,8 +205,8 @@ if not errorlevel == 1 ( ) if "%PRINT_ENV%" == "true" ( - echo Using JAVA_OPTS: !JAVA_OPTS! - echo Using JAVA_RUN_OPTS: !JAVA_RUN_OPTS! + echo Using JAVA_OPTS: !JAVA_OPTS! + echo Using JAVA_RUN_OPTS: !JAVA_RUN_OPTS! ) set START_SERVER=true diff --git a/quarkus/dist/src/main/content/bin/kc.sh b/quarkus/dist/src/main/content/bin/kc.sh index 9a6e62e2dd2e..9c0b946e8522 100644 --- a/quarkus/dist/src/main/content/bin/kc.sh +++ b/quarkus/dist/src/main/content/bin/kc.sh @@ -44,6 +44,7 @@ CLASSPATH_OPTS="'$(abs_path "../lib/quarkus-run.jar")'" DEBUG_MODE="${DEBUG:-false}" DEBUG_PORT="${DEBUG_PORT:-8787}" DEBUG_SUSPEND="${DEBUG_SUSPEND:-n}" +DEBUG_ADDRESS="0.0.0.0:$DEBUG_PORT" esceval() { printf '%s\n' "$1" | sed "s/'/'\\\\''/g; 1 s/^/'/; $ s/$/'/" @@ -55,9 +56,20 @@ do case "$1" in --debug) DEBUG_MODE=true - if [ -n "$2" ] && expr "$2" : '[0-9]\{0,\}$' >/dev/null; then - DEBUG_PORT=$2 - shift + if [ -n "$2" ]; then + # Plain port + if echo "$2" | grep -Eq '^[0-9]+$'; then + DEBUG_ADDRESS="0.0.0.0:$2" + shift + # IPv4 or bracketed IPv6 with optional port + elif echo "$2" | grep -Eq '^(([0-9.]+)|(\[[0-9A-Fa-f:]+\]))'; then + if echo "$2" | grep -Eq ':[0-9]+$'; then + DEBUG_ADDRESS="$2" + else + DEBUG_ADDRESS="$2:$DEBUG_PORT" + fi + shift + fi fi ;; --) @@ -142,7 +154,7 @@ fi if [ "$DEBUG_MODE" = "true" ]; then DEBUG_OPT="$(echo "$JAVA_OPTS" | $GREP "\-agentlib:jdwp")" if [ -z "$DEBUG_OPT" ]; then - JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=$DEBUG_PORT,server=y,suspend=$DEBUG_SUSPEND" + JAVA_OPTS="$JAVA_OPTS -agentlib:jdwp=transport=dt_socket,address=$DEBUG_ADDRESS,server=y,suspend=$DEBUG_SUSPEND" else echo "Debug already enabled in JAVA_OPTS, ignoring --debug argument" fi @@ -152,7 +164,7 @@ esceval_args() { while IFS= read -r entry; do result="$result $(esceval "$entry")" done - echo $result + echo "$result" } JAVA_RUN_OPTS=$(echo "$JAVA_OPTS" | xargs printf '%s\n' | esceval_args) @@ -166,8 +178,8 @@ if [ "$PRINT_ENV" = "true" ]; then fi if [ "$PRE_BUILD" = "true" ]; then - eval "'$JAVA'" -Dkc.config.build-and-exit=true $JAVA_RUN_OPTS || exit $? + eval "'$JAVA'" -Dkc.config.build-and-exit=true "$JAVA_RUN_OPTS" || exit $? JAVA_RUN_OPTS="-Dkc.config.built=true $JAVA_RUN_OPTS" fi -eval exec "'$JAVA'" $JAVA_RUN_OPTS +eval exec "'$JAVA'" "$JAVA_RUN_OPTS" diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java index bc3795d01dd2..ae41030f60df 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/Environment.java @@ -33,9 +33,7 @@ import org.keycloak.common.Profile; import org.keycloak.common.util.NetworkUtils; -import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; import org.keycloak.quarkus.runtime.configuration.Configuration; -import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; public final class Environment { @@ -48,8 +46,6 @@ public final class Environment { public static final String PROD_PROFILE_VALUE = "prod"; public static final String LAUNCH_MODE = "kc.launch.mode"; - private static volatile AbstractCommand parsedCommand; - private Environment() {} public static Boolean isRebuild() { @@ -109,21 +105,6 @@ public static void setProfile(String profile) { } } - /** - * Update the profile settings based upon what was set in the system, environment, or optionally persistent values - */ - public static String updateProfile(boolean usePersistent) { - String profile = org.keycloak.common.util.Environment.getProfile(); - if(profile == null && usePersistent) { - profile = PersistedConfigSource.getInstance().getValue(org.keycloak.common.util.Environment.PROFILE); - } - if (profile == null) { - profile = Environment.PROD_PROFILE_VALUE; - } - setProfile(profile); - return profile; - } - public static boolean isDevMode() { if (org.keycloak.common.util.Environment.isDevMode()) { return true; @@ -247,21 +228,6 @@ public synchronized static Profile getCurrentOrCreateFeatureProfile() { return profile; } - /** - * Get parsed AbstractCommand we obtained from the CLI - */ - public static Optional getParsedCommand() { - return Optional.ofNullable(parsedCommand); - } - - public static boolean isParsedCommand(String commandName) { - return getParsedCommand().filter(f -> f.getName().equals(commandName)).isPresent(); - } - - public static void setParsedCommand(AbstractCommand command) { - Environment.parsedCommand = command; - } - public static void removeHomeDir() { System.getProperties().remove(KC_HOME_DIR); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java index f9d71683a5a5..5211e42e8b3e 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakMain.java @@ -31,16 +31,19 @@ import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; +import org.keycloak.quarkus.runtime.integration.jaxrs.QuarkusKeycloakApplication; import io.quarkus.bootstrap.runner.RunnerClassLoader; +import io.quarkus.arc.Arc; import io.quarkus.runtime.ApplicationLifecycleManager; import io.quarkus.runtime.Quarkus; import org.jboss.logging.Logger; -import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler; import org.keycloak.quarkus.runtime.cli.PropertyException; +import org.keycloak.quarkus.runtime.cli.ExecutionExceptionHandler; import org.keycloak.quarkus.runtime.cli.Picocli; import org.keycloak.common.Version; +import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand; import org.keycloak.quarkus.runtime.cli.command.DryRunMixin; import org.keycloak.quarkus.runtime.cli.command.Start; @@ -54,6 +57,8 @@ @ApplicationScoped public class KeycloakMain implements QuarkusApplication { + private static AbstractNonServerCommand COMMAND; + static { InfinispanUtils.configureVirtualThreads(); } @@ -92,6 +97,9 @@ public static void main(String[] args, Picocli picocli) { } if (cliArgs.isEmpty()) { + if (Environment.isRebuildCheck()) { + return; // nothing to do - not currently caught by the shell scripts + } cliArgs = new ArrayList<>(cliArgs); // default to show help message cliArgs.add("-h"); @@ -128,7 +136,8 @@ private static boolean isFastStart(List cliArgs) { return cliArgs.size() == 2 && cliArgs.get(0).equals(Start.NAME) && cliArgs.stream().anyMatch(OPTIMIZED_BUILD_OPTION_LONG::equals); } - public static void start(Picocli picocli, ExecutionExceptionHandler errorHandler) { + public static void start(Picocli picocli, AbstractNonServerCommand command, ExecutionExceptionHandler errorHandler) { + COMMAND = command; // it would be nice to not do this statically - start quarkus with an instance of KeycloakMain, rather than a class for example try { Quarkus.run(KeycloakMain.class, (exitCode, cause) -> { if (cause != null) { @@ -151,6 +160,10 @@ public static void start(Picocli picocli, ExecutionExceptionHandler errorHandler */ @Override public int run(String... args) throws Exception { + if (COMMAND != null) { + QuarkusKeycloakApplication application = Arc.container().instance(QuarkusKeycloakApplication.class).get(); + COMMAND.onStart(application); + } if (isTestLaunchMode() || isNonServerMode()) { // in test mode we exit immediately // we should be managing this behavior more dynamically depending on the tests requirements (short/long lived) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java index 450ec57a8af0..467cc7d6c03b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/Picocli.java @@ -19,16 +19,13 @@ import static java.lang.String.format; import static org.keycloak.quarkus.runtime.Environment.getProviderFiles; -import static org.keycloak.quarkus.runtime.Environment.isDevMode; import static org.keycloak.quarkus.runtime.Environment.isRebuild; import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck; import static org.keycloak.quarkus.runtime.Environment.isRebuilt; import static org.keycloak.quarkus.runtime.cli.OptionRenderer.decorateDuplicitOptionName; import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG; -import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs; import static org.keycloak.quarkus.runtime.configuration.Configuration.isUserModifiable; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; -import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.maskValue; import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST; import java.io.File; @@ -57,12 +54,9 @@ import org.keycloak.quarkus.runtime.KeycloakMain; import org.keycloak.quarkus.runtime.Messages; import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; +import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand; import org.keycloak.quarkus.runtime.cli.command.Build; -import org.keycloak.quarkus.runtime.cli.command.Completion; import org.keycloak.quarkus.runtime.cli.command.Main; -import org.keycloak.quarkus.runtime.cli.command.ShowConfig; -import org.keycloak.quarkus.runtime.cli.command.StartDev; -import org.keycloak.quarkus.runtime.cli.command.UpdateCompatibility; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor; @@ -105,6 +99,7 @@ private static class IncludeOptions { private final ExecutionExceptionHandler errorHandler = new ExecutionExceptionHandler(); private Set> allowedMappers; private final List unrecognizedArgs = new ArrayList<>(); + private Optional parsedCommand = Optional.empty(); public void parseAndRun(List cliArgs) { // perform two passes over the cli args. First without option validation to determine the current command, then with option validation enabled @@ -118,17 +113,7 @@ public void parseAndRun(List cliArgs) { // recreate the command specifically for the current cmd = createCommandLineForCommand(cliArgs, commandLineList); - int exitCode; - if (isRebuildCheck()) { - CommandLine currentCommand = null; - if (commandLineList.size() > 1) { - currentCommand = commandLineList.get(commandLineList.size() - 1); - } - exitCode = runReAugmentationIfNeeded(cliArgs, cmd, currentCommand); - } else { - PropertyMappers.sanitizeDisabledMappers(); - exitCode = cmd.execute(argArray); - } + int exitCode = cmd.execute(argArray); exit(exitCode); } catch (ParameterException parEx) { @@ -168,15 +153,16 @@ public T set(T value) { addHelp(currentSpec); } + AbstractCommand currentCommand = null; if (currentSpec != null) { CommandLine commandLine = currentSpec.commandLine(); addCommandOptions(cliArgs, commandLine); if (commandLine != null && commandLine.getCommand() instanceof AbstractCommand ac) { - // set current parsed command - Environment.setParsedCommand(ac); + currentCommand = ac; } } + initConfig(currentCommand); if (isRebuildCheck()) { // build command should be available when running re-aug @@ -185,6 +171,10 @@ public T set(T value) { }); } + public Optional getParsedCommand() { + return parsedCommand; + } + private void catchParameterException(ParameterException parEx, CommandLine cmd, String[] args) { int exitCode; try { @@ -205,122 +195,6 @@ public void exit(int exitCode) { System.exit(exitCode); } - private int runReAugmentationIfNeeded(List cliArgs, CommandLine cmd, CommandLine currentCommand) { - int exitCode = 0; - - if (currentCommand == null) { - return exitCode; // possible if using --version or the user made a mistake - } - - String currentCommandName = currentCommand.getCommandName(); - - if (shouldSkipRebuild(cliArgs, currentCommandName)) { - return exitCode; - } - - // TODO: ensure that the config has not yet been initialized - // - there's currently no good way to do that directly on ConfigProviderResolver - initProfile(cliArgs, currentCommandName); - - if (requiresReAugmentation(currentCommand)) { - PropertyMappers.sanitizeDisabledMappers(); - exitCode = runReAugmentation(cliArgs, cmd); - } - - return exitCode; - } - - protected void initProfile(List cliArgs, String currentCommandName) { - if (currentCommandName.equals(StartDev.NAME)) { - // force the server image to be set with the dev profile - Environment.forceDevProfile(); - } else { - Environment.updateProfile(false); - - // override from the cli if specified - parseConfigArgs(cliArgs, (k, v) -> { - if (k.equals(Main.PROFILE_SHORT_NAME) || k.equals(Main.PROFILE_LONG_NAME)) { - Environment.setProfile(v); - } - }, ignored -> {}); - } - } - - private static boolean shouldSkipRebuild(List cliArgs, String currentCommandName) { - return cliArgs.contains("--help") - || cliArgs.contains("-h") - || cliArgs.contains("--help-all") - || currentCommandName.equals(Build.NAME) - || currentCommandName.equals(ShowConfig.NAME) - || currentCommandName.equals(Completion.NAME) - || currentCommandName.equals(UpdateCompatibility.NAME); - } - - private static boolean requiresReAugmentation(CommandLine cmdCommand) { - Map rawPersistedProperties = Configuration.getRawPersistedProperties(); - if (rawPersistedProperties.isEmpty()) { - return true; // no build yet - } - var current = getNonPersistedBuildTimeOptions(); - - // everything but the optimized value must match - String key = Configuration.KC_OPTIMIZED; - Optional.ofNullable(rawPersistedProperties.get(key)).ifPresentOrElse(value -> current.put(key, value), () -> current.remove(key)); - return !rawPersistedProperties.equals(current); - } - - /** - * checks the raw cli input for possible credentials / properties which should be masked, - * and masks them. - * @return a list of potentially masked properties in CLI format, e.g. `--db-password=*******` - * instead of the actual passwords value. - */ - private static List getSanitizedRuntimeCliOptions() { - List properties = new ArrayList<>(); - - parseConfigArgs(ConfigArgsConfigSource.getAllCliArgs(), (key, value) -> { - PropertyMapper mapper = PropertyMappers.getMapperByCliKey(key); - - if (mapper == null || mapper.isRunTime()) { - properties.add(key + "=" + maskValue(value, mapper)); - } - }, properties::add); - - return properties; - } - - private static int runReAugmentation(List cliArgs, CommandLine cmd) { - if (cmd == null) { - throw new IllegalStateException("CommandLine is null when trying to run re-augmentation. (CLI args: '%s')".formatted(String.join(", ", cliArgs))); - } - - if (!isDevMode()) { - cmd.getOut().println("Changes detected in configuration. Updating the server image."); - if (Configuration.isOptimized()) { - checkChangesInBuildOptionsDuringAutoBuild(cmd.getOut()); - } - } - - List configArgsList = new ArrayList<>(); - configArgsList.add(Build.NAME); - parseConfigArgs(cliArgs, (k, v) -> { - PropertyMapper mapper = PropertyMappers.getMapperByCliKey(k); - - if (mapper != null && mapper.isBuildTime()) { - configArgsList.add(k + "=" + v); - } - }, ignored -> {}); - - cmd = cmd.setUnmatchedArgumentsAllowed(true); - int exitCode = cmd.execute(configArgsList.toArray(new String[0])); - - if(!isDevMode() && exitCode == cmd.getCommandSpec().exitCodeOnSuccess()) { - cmd.getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG); - } - - return exitCode; - } - private static boolean wasBuildEverRun() { return !Configuration.getRawPersistedProperties().isEmpty(); } @@ -623,6 +497,11 @@ public void info(String text) { getOutWriter().println(defaultColorScheme.apply("INFO: ", Arrays.asList(Style.fg_green, Style.bold)) + text); } + public void error(String text) { + ColorScheme defaultColorScheme = picocli.CommandLine.Help.defaultColorScheme(Help.Ansi.AUTO); + getErrWriter().println(defaultColorScheme.apply(text, Arrays.asList(Style.fg_red, Style.bold))); + } + private static void warn(String text, PrintWriter outwriter) { ColorScheme defaultColorScheme = picocli.CommandLine.Help.defaultColorScheme(Help.Ansi.AUTO); outwriter.println(defaultColorScheme.apply("WARNING: ", Arrays.asList(Style.fg_yellow, Style.bold)) + text); @@ -922,8 +801,8 @@ private static String getDecoratedOptionDescription(PropertyMapper mapper) { return transformedDesc.toString(); } - public static void println(CommandLine cmd, String message) { - cmd.getOut().println(message); + public void println(String message) { + getOutWriter().println(message); } public static List parseArgs(String[] rawArgs) throws PropertyException { @@ -953,7 +832,7 @@ public static List parseArgs(String[] rawArgs) throws PropertyException return args; } - private static void checkChangesInBuildOptionsDuringAutoBuild(PrintWriter out) { + public static void checkChangesInBuildOptionsDuringAutoBuild(PrintWriter out) { StringBuilder options = new StringBuilder(); checkChangesInBuildOptions((key, oldValue, newValue) -> optionChanged(options, key, oldValue, newValue)); @@ -1011,11 +890,26 @@ private static boolean isIgnoredPersistedOption(String key) { } public void start() { - KeycloakMain.start(this, errorHandler); + KeycloakMain.start(this, (AbstractNonServerCommand) this.getParsedCommand() + .filter(AbstractNonServerCommand.class::isInstance).orElse(null), this.errorHandler); } public void build() throws Throwable { QuarkusEntryPoint.main(); } + public void initConfig(AbstractCommand command) { + if (Configuration.isInitialized()) { + throw new IllegalStateException("Config should not be initialized until profile is determined"); + } + this.parsedCommand = Optional.ofNullable(command); + + String profile = parsedCommand.map(AbstractCommand::getInitProfile) + .orElseGet(() -> Optional.ofNullable(org.keycloak.common.util.Environment.getProfile()) + .orElse(Environment.PROD_PROFILE_VALUE)); + + Environment.setProfile(profile); + parsedCommand.ifPresent(PropertyMappers::sanitizeDisabledMappers); + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java index 6c7926bd9aa9..586a622f3326 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractCommand.java @@ -22,16 +22,19 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; +import java.util.concurrent.Callable; import org.keycloak.config.OptionCategory; +import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.Picocli; import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; +import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; import picocli.CommandLine.Spec; -public abstract class AbstractCommand { +public abstract class AbstractCommand implements Callable { @Spec protected CommandSpec spec; // will be null for "start --optimized" @@ -45,6 +48,52 @@ protected void executionError(CommandLine cmd, String message, Throwable cause) cliExecutionError(cmd, message, cause); } + /** + * Get the effective profile used when the config is initialized + */ + public String getInitProfile() { + String configuredProfile = org.keycloak.common.util.Environment.getProfile(); + if (configuredProfile != null) { + return configuredProfile; // the profile was already set by the cli or even ENV + } + if (Environment.isRebuildCheck()) { + // builds default to prod, if the profile is not overriden via the cli + return Environment.PROD_PROFILE_VALUE; + } + // otherwise take the default profile, or what is persisted, or ultimately prod + return Optional.ofNullable(this.getDefaultProfile()) + .or(() -> Optional.ofNullable( + PersistedConfigSource.getInstance().getValue(org.keycloak.common.util.Environment.PROFILE))) + .orElse(Environment.PROD_PROFILE_VALUE); + } + + @Override + public Integer call() { + return callCommand().orElseGet(() -> { + runCommand(); + return CommandLine.ExitCode.OK; + }); + } + + /** + * An alternative to {@link #runCommand()} that allows for returning an exit code. + * If the Optional is empty, {@link #runCommand()} will still be called + *
+ * see {@link #call()} + */ + protected Optional callCommand() { + return Optional.empty(); + } + + /** + * If {@link #callCommand()} returns an empty {@link Optional}, then this method will be used to run the command. OK will be returned as the exit code after successful completion. + *
+ * see {@link #call()} + */ + protected void runCommand() { + + } + /** * Returns true if this command should include runtime options for the CLI. */ @@ -80,4 +129,12 @@ public void setPicocli(Picocli picocli) { this.picocli = picocli; } + /** + * The default profile for the command, or null if the persisted profile should be checked first + * @return + */ + protected String getDefaultProfile() { + return Environment.PROD_PROFILE_VALUE; + } + } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java index dcdf7d43418e..405c31578acf 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractNonServerCommand.java @@ -26,19 +26,14 @@ import java.util.List; import java.util.stream.Collectors; -public abstract class AbstractNonServerCommand extends AbstractStartCommand implements Runnable { +public abstract class AbstractNonServerCommand extends AbstractStartCommand { @CommandLine.Mixin - OptimizedMixin optimizedMixin; - - @CommandLine.Mixin - HelpAllMixin helpAllMixin; + OptimizedMixin optimizedMixin = new OptimizedMixin(); @Override - public void run() { - Environment.setProfile(Environment.NON_SERVER_MODE); - - super.run(); + public String getDefaultProfile() { + return Environment.NON_SERVER_MODE; } @Override diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java index 98c1cd87d321..c69b1ae9f512 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractStartCommand.java @@ -17,27 +17,111 @@ package org.keycloak.quarkus.runtime.cli.command; -import org.keycloak.config.OptionCategory; -import org.keycloak.quarkus.runtime.configuration.mappers.HostnameV2PropertyMappers; -import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; +import static org.keycloak.quarkus.runtime.Environment.isDevMode; +import static org.keycloak.quarkus.runtime.Environment.isDevProfile; +import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck; +import static org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource.parseConfigArgs; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.maskValue; +import java.util.ArrayList; import java.util.EnumSet; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +import org.keycloak.config.OptionCategory; +import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.cli.Picocli; +import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.quarkus.runtime.configuration.mappers.HostnameV2PropertyMappers; +import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; + import picocli.CommandLine; import picocli.CommandLine.Help.Ansi; -import static org.keycloak.quarkus.runtime.Environment.isDevProfile; +public abstract class AbstractStartCommand extends AbstractCommand { -public abstract class AbstractStartCommand extends AbstractCommand implements Runnable { public static final String OPTIMIZED_BUILD_OPTION_LONG = "--optimized"; @CommandLine.Mixin DryRunMixin dryRunMixin = new DryRunMixin(); + @CommandLine.Mixin + HelpAllMixin helpAllMixin; + @Override - public void run() { + protected Optional callCommand() { + if (isRebuildCheck()) { + if (requiresReAugmentation()) { + runReAugmentation(); + } + return Optional.of(CommandLine.ExitCode.OK); + } + return Optional.empty(); + } + + static boolean requiresReAugmentation() { + Map rawPersistedProperties = Configuration.getRawPersistedProperties(); + if (rawPersistedProperties.isEmpty()) { + return true; // no build yet + } + var current = Picocli.getNonPersistedBuildTimeOptions(); + + // everything but the optimized value must match + String key = Configuration.KC_OPTIMIZED; + Optional.ofNullable(rawPersistedProperties.get(key)).ifPresentOrElse(value -> current.put(key, value), () -> current.remove(key)); + return !rawPersistedProperties.equals(current); + } + + private void runReAugmentation() { + if(!isDevMode()) { + spec.commandLine().getOut().println("Changes detected in configuration. Updating the server image."); + if (Configuration.isOptimized()) { + Picocli.checkChangesInBuildOptionsDuringAutoBuild(spec.commandLine().getOut()); + } + } + + directBuild(); + + if(!isDevMode()) { + spec.commandLine().getOut().printf("Next time you run the server, just run:%n%n\t%s %s %s%n%n", Environment.getCommand(), String.join(" ", getSanitizedRuntimeCliOptions()), OPTIMIZED_BUILD_OPTION_LONG); + } + } + + public void directBuild() { + Build build = new Build(); + build.dryRunMixin = this.dryRunMixin; + build.setPicocli(picocli); + build.spec = spec; + build.runCommand(); + } + + /** + * checks the raw cli input for possible credentials / properties which should be masked, + * and masks them. + * @return a list of potentially masked properties in CLI format, e.g. `--db-password=*******` + * instead of the actual passwords value. + */ + private static List getSanitizedRuntimeCliOptions() { + List properties = new ArrayList<>(); + + parseConfigArgs(ConfigArgsConfigSource.getAllCliArgs(), (key, value) -> { + PropertyMapper mapper = PropertyMappers.getMapperByCliKey(key); + + if (mapper == null || mapper.isRunTime()) { + properties.add(key + "=" + maskValue(value, mapper)); + } + }, properties::add); + + return properties; + } + + @Override + protected void runCommand() { doBeforeRun(); validateConfig(); @@ -45,11 +129,18 @@ public void run() { picocli.getOutWriter().println(Ansi.AUTO.string( "@|bold,red Running the server in development mode. DO NOT use this configuration in production.|@")); } - if (!Boolean.TRUE.equals(dryRunMixin.dryRun)) { + if (shouldStart() && !Boolean.TRUE.equals(dryRunMixin.dryRun)) { picocli.start(); } } + /** + * Controls whether the command actually starts the server + */ + protected boolean shouldStart() { + return true; + } + protected void doBeforeRun() { } @@ -57,8 +148,10 @@ protected void doBeforeRun() { @Override protected void validateConfig() { super.validateConfig(); // we want to run the generic validation here first to check for unknown options - HttpPropertyMappers.validateConfig(); - HostnameV2PropertyMappers.validateConfig(picocli); + if (shouldStart()) { // if not starting, we aren't accepting http requests so no validation needed + HttpPropertyMappers.validateConfig(); + HostnameV2PropertyMappers.validateConfig(picocli); + } } @Override diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java index 7aeb782ff1dc..755bf9f822ca 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/AbstractUpdatesCommand.java @@ -19,51 +19,41 @@ import java.io.File; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.ServiceLoader; -import java.util.function.Predicate; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.compatibility.CompatibilityMetadataProvider; import org.keycloak.config.ConfigProviderFactory; -import org.keycloak.config.OptionCategory; -import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.cli.PropertyException; import picocli.CommandLine; -public abstract class AbstractUpdatesCommand extends AbstractCommand implements Runnable { +public abstract class AbstractUpdatesCommand extends AbstractStartCommand { private static final int FEATURE_DISABLED_EXIT_CODE = 4; @CommandLine.Mixin - HelpAllMixin helpAllMixin; - - @CommandLine.Mixin - OptimizedMixin optimizedMixin; + OptimizedMixin optimizedMixin = new OptimizedMixin(); @Override - public List getOptionCategories() { - return super.getOptionCategories().stream() - .filter(Predicate.not(OptionCategory.EXPORT::equals)) - .filter(Predicate.not(OptionCategory.IMPORT::equals)) - .toList(); + protected boolean shouldStart() { + return false; } @Override - public void run() { - Environment.updateProfile(true); - if (!Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V1)) { - printFeatureDisabled(); - picocli.exit(FEATURE_DISABLED_EXIT_CODE); - return; - } - loadConfiguration(); - printPreviewWarning(); - validateConfig(); - var exitCode = executeAction(); - picocli.exit(exitCode); + protected Optional callCommand() { + return super.callCommand().or(() -> { + if (!Profile.isAnyVersionOfFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V1)) { + printFeatureDisabled(); + return Optional.of(FEATURE_DISABLED_EXIT_CODE); + } + loadConfiguration(); + printPreviewWarning(); + validateConfig(); + return Optional.of(executeAction()); + }); } abstract int executeAction(); @@ -74,33 +64,14 @@ static void validateFileIsNotDirectory(File file, String option) { } } - void printOut(String message) { - var cmd = getCommandLine(); - if (cmd.isPresent()) { - cmd.get().getOut().println(message); - } else { - System.out.println(message); - } - } - - void printError(String message) { - var cmd = getCommandLine(); - if (cmd.isPresent()) { - var colorScheme = cmd.get().getColorScheme(); - cmd.get().getErr().println(colorScheme.errorText(message)); - } else { - System.err.println(message); - } - } - private void printPreviewWarning() { if (Profile.isFeatureEnabled(Profile.Feature.ROLLING_UPDATES_V2) && (Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.PREVIEW || Profile.Feature.ROLLING_UPDATES_V2.getType() == Profile.Feature.Type.EXPERIMENTAL)) { - printError("Warning! This command is '" + Profile.Feature.ROLLING_UPDATES_V2.getType() + "' and is not recommended for use in production. It may change or be removed at a future release."); + picocli.error("Warning! This command is '" + Profile.Feature.ROLLING_UPDATES_V2.getType() + "' and is not recommended for use in production. It may change or be removed at a future release."); } } void printFeatureDisabled() { - printError("Unable to use this command. None of the versions of the feature '" + Profile.Feature.ROLLING_UPDATES_V1.getUnversionedKey() + "' is enabled."); + picocli.error("Unable to use this command. None of the versions of the feature '" + Profile.Feature.ROLLING_UPDATES_V1.getUnversionedKey() + "' is enabled."); } static Map loadAllProviders() { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java index a697f1a98c1d..c013ac89b93b 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Build.java @@ -21,7 +21,6 @@ import static org.keycloak.config.DatabaseOptions.DB; import static org.keycloak.quarkus.runtime.Environment.getHomePath; import static org.keycloak.quarkus.runtime.Environment.isDevProfile; -import static org.keycloak.quarkus.runtime.cli.Picocli.println; import io.quarkus.runtime.LaunchMode; @@ -58,7 +57,7 @@ + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --metrics-enabled=true%n%n" + " Change the relative path:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --http-relative-path=/auth%n") -public final class Build extends AbstractCommand implements Runnable { +public final class Build extends AbstractCommand { public static final String NAME = "build"; @@ -69,10 +68,7 @@ public final class Build extends AbstractCommand implements Runnable { DryRunMixin dryRunMixin; @Override - public void run() { - if (org.keycloak.common.util.Environment.getProfile() == null) { - Environment.setProfile(Environment.PROD_PROFILE_VALUE); - } + protected void runCommand() { checkProfileAndDb(); // validate before setting that we're rebuilding so that runtime options are still seen @@ -82,7 +78,7 @@ public void run() { }); System.setProperty("quarkus.launch.rebuild", "true"); - println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait."); + picocli.println("Updating the configuration and installing your custom providers, if any. Please wait."); try { configureBuildClassLoader(); @@ -95,8 +91,8 @@ public void run() { } if (!isDevProfile()) { - println(spec.commandLine(), "Server configuration updated and persisted. Run the following command to review the configuration:\n"); - println(spec.commandLine(), "\t" + Environment.getCommand() + " show-config\n"); + picocli.println("Server configuration updated and persisted. Run the following command to review the configuration:\n"); + picocli.println("\t" + Environment.getCommand() + " show-config\n"); } } catch (Throwable throwable) { executionError(spec.commandLine(), "Failed to update server configuration.", throwable); @@ -119,7 +115,7 @@ public boolean includeBuildTime() { private void checkProfileAndDb() { if (Environment.isDevProfile()) { - String cmd = Environment.getParsedCommand().map(AbstractCommand::getName).orElse(getName()); + String cmd = picocli.getParsedCommand().map(AbstractCommand::getName).orElse(getName()); // we allow start-dev, and import|export|bootstrap-admin --profile=dev // but not start --profile=dev, nor build --profile=dev if (Start.NAME.equals(cmd) || Build.NAME.equals(cmd)) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java index 9bf397c9b4a9..7264af3f17fd 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Export.java @@ -29,7 +29,7 @@ @Command(name = Export.NAME, header = "Export data from realms to a file or directory.", description = "%nExport data from realms to a file or directory.") -public final class Export extends AbstractNonServerCommand implements Runnable { +public final class Export extends AbstractNonServerCommand { public static final String NAME = "export"; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java index cc606a06b9ef..e1a6b53301e0 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Import.java @@ -29,7 +29,7 @@ @Command(name = Import.NAME, header = "Import data from a directory or a file.", description = "%nImport data from a directory or a file.") -public final class Import extends AbstractNonServerCommand implements Runnable { +public final class Import extends AbstractNonServerCommand { public static final String NAME = "import"; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java index 415d0534ed96..cc90cfd8cec8 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java @@ -41,7 +41,7 @@ @Command(name = "show-config", header = "Print out the current configuration.", description = "%nPrint out the current configuration.") -public final class ShowConfig extends AbstractCommand implements Runnable { +public final class ShowConfig extends AbstractCommand { public static final String NAME = "show-config"; private static final List allowedSystemPropertyKeys = List.of( @@ -54,8 +54,13 @@ public final class ShowConfig extends AbstractCommand implements Runnable { String filter; @Override - public void run() { - String profile = Environment.updateProfile(true); + public String getDefaultProfile() { + return null; + } + + @Override + protected void runCommand() { + String profile = org.keycloak.common.util.Environment.getProfile(); spec.commandLine().getOut().printf("Current Mode: %s%n", Environment.getKeycloakModeFromProfile(profile)); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java index 3221d26dcae8..a6f7b67da573 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/Start.java @@ -24,7 +24,6 @@ import org.keycloak.common.profile.ProfileException; import org.keycloak.quarkus.runtime.cli.Picocli; import org.keycloak.quarkus.runtime.cli.PropertyException; -import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -37,7 +36,7 @@ footer = "%nBy default, this command tries to update the server configuration by running a '" + Build.NAME + "' before starting the server. You can disable this behavior by using the '" + OPTIMIZED_BUILD_OPTION_LONG + "' option:%n%n" + " $ ${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} '" + OPTIMIZED_BUILD_OPTION_LONG + "'%n%n" + "By doing that, the server should start faster based on any previous configuration you have set when manually running the '" + Build.NAME + "' command.") -public final class Start extends AbstractStartCommand implements Runnable { +public final class Start extends AbstractStartCommand { public static final String NAME = "start"; @@ -47,12 +46,8 @@ public final class Start extends AbstractStartCommand implements Runnable { @CommandLine.Mixin ImportRealmMixin importRealmMixin; - @CommandLine.Mixin - HelpAllMixin helpAllMixin; - @Override protected void doBeforeRun() { - Environment.updateProfile(true); if (Environment.isDevProfile()) { throw new PropertyException(Messages.devProfileNotAllowedError(NAME)); } @@ -71,12 +66,11 @@ public String getName() { public static void fastStart(Picocli picocli, boolean dryRun) { try { Start start = new Start(); - Environment.setParsedCommand(start); - PropertyMappers.sanitizeDisabledMappers(); start.optimizedMixin.optimized = true; start.dryRunMixin.dryRun = dryRun; start.setPicocli(picocli); - start.run(); + picocli.initConfig(start); + picocli.exit(start.call()); } catch (PropertyException | ProfileException e) { picocli.usageException(e.getMessage(), e.getCause()); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java index 60af84ae1da7..0a2fee46a9a2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/StartDev.java @@ -17,11 +17,10 @@ package org.keycloak.quarkus.runtime.cli.command; -import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.common.util.Environment; import picocli.CommandLine; import picocli.CommandLine.Command; -import picocli.CommandLine.Mixin; @Command(name = StartDev.NAME, header = "Start the server in development mode.", @@ -30,19 +29,21 @@ }, footer = "%nDo NOT start the server using this command when deploying to production.%n%n" + "Use '${PARENT-COMMAND-FULL-NAME:-$PARENTCOMMAND} ${COMMAND-NAME} --help-all' to list all available options, including build options.") -public final class StartDev extends AbstractStartCommand implements Runnable { +public final class StartDev extends AbstractStartCommand { public static final String NAME = "start-dev"; - @Mixin - HelpAllMixin helpAllMixin; - @CommandLine.Mixin ImportRealmMixin importRealmMixin; @Override - protected void doBeforeRun() { - Environment.forceDevProfile(); + public String getInitProfile() { + return Environment.DEV_PROFILE_VALUE; // only ever dev - could be a validation instead + } + + @Override + public String getDefaultProfile() { + return Environment.DEV_PROFILE_VALUE; } @Override diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java index 53c0c2220966..853fdbae22bb 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityCheck.java @@ -55,20 +55,20 @@ int executeAction() { var id = idIterator.next(); var provider = providers.get(id); if (provider == null) { - printError("[%s] Provider not found. Rolling Update is not available.".formatted(id)); + picocli.error("[%s] Provider not found. Rolling Update is not available.".formatted(id)); return CompatibilityResult.ExitCode.RECREATE.value(); } var result = provider.isCompatible(Map.copyOf(info.getOrDefault(id, Map.of()))); - result.endMessage().ifPresent(this::printOut); + result.endMessage().ifPresent(picocli.getOutWriter()::println); if (Util.isNotCompatible(result)) { - result.errorMessage().ifPresent(this::printError); + result.errorMessage().ifPresent(picocli::error); return result.exitCode(); } } - printOut("[OK] Rolling Update is available."); + picocli.getOutWriter().println("[OK] Rolling Update is available."); return CompatibilityResult.ExitCode.ROLLING.value(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java index 2672cac0e4b5..4d88b203f7db 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/UpdateCompatibilityMetadata.java @@ -83,7 +83,7 @@ private void validateFileParameter() { private void printToConsole(Map> metadata) { try { var json = JsonSerialization.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(metadata); - printOut("Metadata:%n%s".formatted(json)); + picocli.getOutWriter().println("Metadata:%n%s".formatted(json)); } catch (JsonProcessingException e) { throw new PropertyException("Unable to create JSON representation of the metadata", e); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java index 7ee801bb1833..19ee8706f48f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java @@ -89,6 +89,10 @@ public static boolean equals(Option option, String value) { .isPresent(); } + public static boolean isInitialized() { + return config != null; + } + public static synchronized SmallRyeConfig getConfig() { if (config == null) { config = ConfigUtils.emptyConfigBuilder().addDiscoveredSources().build(); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java index 4cf15fd6c1d1..a9aed65de6f4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java @@ -1,5 +1,8 @@ package org.keycloak.quarkus.runtime.configuration.mappers; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; +import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; + import java.io.File; import java.nio.file.Files; import java.nio.file.Paths; @@ -8,8 +11,6 @@ import java.util.Optional; import java.util.function.BooleanSupplier; -import com.google.common.base.CaseFormat; -import io.smallrye.config.ConfigSourceInterceptorContext; import org.keycloak.common.Profile; import org.keycloak.config.CachingOptions; import org.keycloak.config.Option; @@ -19,8 +20,9 @@ import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.utils.StringUtil; -import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalKcValue; -import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; +import com.google.common.base.CaseFormat; + +import io.smallrye.config.ConfigSourceInterceptorContext; final class CachingPropertyMappers { @@ -98,6 +100,26 @@ public static PropertyMapper[] getClusteringPropertyMappers() { .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_EMBEDDED_NETWORK_BIND_ADDRESS) + .paramLabel("address") + .to("kc.spi-cache-embedded--default--network-bind-address") + .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled") + .build(), + fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_BIND_PORT) + .paramLabel("port") + .to("kc.spi-cache-embedded--default--network-bind-port") + .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled") + .build(), + fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_ADDRESS) + .paramLabel("address") + .to("kc.spi-cache-embedded--default--network-external-address") + .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled") + .build(), + fromOption(CachingOptions.CACHE_EMBEDDED_NETWORK_EXTERNAL_PORT) + .paramLabel("port") + .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, "Infinispan clustered embedded is enabled") + .to("kc.spi-cache-embedded--default--network-external-port") + .build(), fromOption(CachingOptions.CACHE_REMOTE_HOST) .paramLabel("hostname") .to("kc.spi-cache-remote--default--hostname") diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index 8c12ffa3e441..986ffce47c4c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -15,10 +15,12 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; +import static org.keycloak.config.DatabaseOptions.DB; import static org.keycloak.config.DatabaseOptions.OPTIONS_DATASOURCES; import static org.keycloak.config.DatabaseOptions.getDatasourceOption; import static org.keycloak.config.DatabaseOptions.getKeyForDatasource; @@ -51,6 +53,11 @@ public static PropertyMapper[] getDatabasePropertyMappers() { .mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getDatabaseUrl) .paramLabel("jdbc-url") .build(), + fromOption(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE) + .to("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType") + .mapFrom(DatabaseOptions.DB, DatabasePropertyMappers::getPostgresqlTargetServerType) + .isEnabled(() -> getPostgresqlTargetServerType(Configuration.getConfigValue(DB).getValue(), null) != null) + .build(), fromOption(DatabaseOptions.DB_URL_HOST) .paramLabel("hostname") .build(), @@ -114,6 +121,33 @@ public static PropertyMapper[] getDatabasePropertyMappers() { .description("Used for internal purposes of H2 database.") .build(); + private static String getPostgresqlTargetServerType(String db, ConfigSourceInterceptorContext context) { + Database.Vendor vendor = Database.getVendor(db).orElse(null); + if (vendor != Database.Vendor.POSTGRES) { + return null; + } + + String dbDriver = Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue(); + String dbUrl = Configuration.getConfigValue(DatabaseOptions.DB_URL).getValue(); + String dbUrlProperties = Configuration.getConfigValue(DatabaseOptions.DB_URL_PROPERTIES).getValue(); + + if (!Objects.equals(Database.getDriver(db, true).orElse(null), dbDriver) && + !Objects.equals(Database.getDriver(db, false).orElse(null), dbDriver)) { + // Custom JDBC-Driver, for example, AWS JDBC Wrapper. + return null; + } + if (dbUrlProperties != null && dbUrl != null && dbUrl.contains("${kc.db-url-properties:}") && dbUrlProperties.contains("targetServerType")) { + // targetServerType already set to same or different value in db-url-properties, ignore + return null; + } + if (dbUrl != null && dbUrl.contains("targetServerType")) { + // targetServerType already set to same or different value in db-url, ignore + return null; + } + log.debug("setting targetServerType for PostgreSQL to 'primary'"); + return "primary"; + } + private static String getDatabaseUrl(String name, String value, ConfigSourceInterceptorContext c) { return Database.getDefaultUrl(name, value).orElse(null); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java index 80b1e5a2ae4d..7519c1b74d9f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/HttpPropertyMappers.java @@ -93,8 +93,7 @@ public static PropertyMapper[] getHttpPropertyMappers() { .build(), fromOption(HttpOptions.HTTPS_CERTIFICATES_RELOAD_PERIOD) .to("quarkus.http.ssl.certificate.reload-period") - // -1 means no reload - .transformer((value, context) -> "-1".equals(value) ? null : value) + .transformer(HttpPropertyMappers::transformNegativeReloadPeriod) .paramLabel("reload period") .build(), fromOption(HttpOptions.HTTPS_CERTIFICATE_FILE) @@ -178,6 +177,11 @@ private static String getHttpEnabledTransformer(String value, ConfigSourceInterc return isHttpEnabled(value) ? "enabled" : "disabled"; } + static String transformNegativeReloadPeriod(String value, ConfigSourceInterceptorContext context) { + // -1 means no reload + return "-1".equals(value) ? null : value; + } + private static boolean isHttpEnabled(String value) { if (Environment.isDevMode() || Environment.isNonServerMode()) { return true; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java index 7815bc6d9e23..e973a097419d 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/ManagementPropertyMappers.java @@ -72,8 +72,7 @@ public static PropertyMapper[] getManagementPropertyMappers() { fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATES_RELOAD_PERIOD) .mapFrom(HttpOptions.HTTPS_CERTIFICATES_RELOAD_PERIOD) .to("quarkus.management.ssl.certificate.reload-period") - // -1 means no reload - .transformer((value, context) -> "-1".equals(value) ? null : value) + .transformer(HttpPropertyMappers::transformNegativeReloadPeriod) .paramLabel("reload period") .build(), fromOption(ManagementOptions.HTTPS_MANAGEMENT_CERTIFICATE_FILE) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index bb432027b513..2f76d86203fc 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -258,7 +258,8 @@ private ConfigValue transformValue(String name, ConfigValue configValue, ConfigS String mappedValue = value; boolean mapped = false; - var theMapper = parentValue ? this.parentMapper : this.mapper; + // use parent mapper/transformer when no mapper is explicitly specified in .mapFrom() + var theMapper = parentValue && parentMapper != null ? this.parentMapper : this.mapper; if (theMapper != null && (!name.equals(getFrom()) || parentValue)) { mappedValue = theMapper.map(getNamedProperty().orElse(null), value, context); mapped = true; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java index 3fc5e9214dc0..ee914a182f52 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMappers.java @@ -12,7 +12,6 @@ import org.keycloak.quarkus.runtime.cli.Picocli; import org.keycloak.quarkus.runtime.cli.PropertyException; import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; -import org.keycloak.quarkus.runtime.cli.command.Build; import org.keycloak.quarkus.runtime.cli.command.ShowConfig; import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; @@ -34,7 +33,6 @@ import java.util.Set; import java.util.stream.Collectors; -import static org.keycloak.quarkus.runtime.Environment.isParsedCommand; import static org.keycloak.quarkus.runtime.Environment.isRebuild; import static org.keycloak.quarkus.runtime.Environment.isRebuildCheck; import static org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider.isKeyStoreConfigSource; @@ -138,8 +136,8 @@ public static Map> getDisabledBuildTimeMappers() { /** * Removes all disabled mappers from the runtime/buildtime mappers */ - public static void sanitizeDisabledMappers() { - MAPPERS.sanitizeDisabledMappers(); + public static void sanitizeDisabledMappers(AbstractCommand command) { + MAPPERS.sanitizeDisabledMappers(command); } public static String maskValue(String value, PropertyMapper mapper) { @@ -226,11 +224,8 @@ public static boolean isDisabledMapper(String property) { return getDisabledMapper(property).isPresent() && getMapper(property) == null; } - private static Set> filterDeniedCategories(List> mappers) { - final var allowedCategories = Environment.getParsedCommand() - .map(AbstractCommand::getOptionCategories) - .map(EnumSet::copyOf) - .orElseGet(() -> EnumSet.allOf(OptionCategory.class)); + private static Set> filterDeniedCategories(List> mappers, AbstractCommand command) { + final var allowedCategories = EnumSet.copyOf(command.getOptionCategories()); return mappers.stream().filter(f -> allowedCategories.contains(f.getCategory())).collect(Collectors.toSet()); } @@ -326,11 +321,7 @@ public Set> getWildcardMappers() { return Collections.unmodifiableSet(wildcardMappers); } - public void sanitizeDisabledMappers() { - if (Environment.getParsedCommand().isEmpty()) { - return; // do not sanitize when no command is present - } - + public void sanitizeDisabledMappers(AbstractCommand command) { DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available // Initialize profile in order to check state of features. Disable Persisted CS for re-augmentation @@ -343,22 +334,22 @@ public void sanitizeDisabledMappers() { sanitizeMappers(buildTimeMappers, disabledBuildTimeMappers); sanitizeMappers(runtimeTimeMappers, disabledRuntimeMappers); - assertDuplicatedMappers(); + assertDuplicatedMappers(command); }); } - private void assertDuplicatedMappers() { + private void assertDuplicatedMappers(AbstractCommand command) { final var duplicatedMappers = entrySet().stream() .filter(e -> CollectionUtil.isNotEmpty(e.getValue())) .filter(e -> e.getValue().size() > 1) .toList(); - final var isBuildPhase = isRebuild() || isRebuildCheck() || isParsedCommand(Build.NAME); - final var allowedForCommand = isParsedCommand(ShowConfig.NAME); + final var isBuildPhase = isRebuild() || isRebuildCheck() || command.includeBuildTime(); + final var allowedForCommand = ShowConfig.NAME.equals(command.getName()); if (!duplicatedMappers.isEmpty()) { duplicatedMappers.forEach(f -> { - final var filteredMappers = filterDeniedCategories(f.getValue()); + final var filteredMappers = filterDeniedCategories(f.getValue(), command); if (filteredMappers.size() > 1) { final var areBuildTimeMappers = filteredMappers.stream().anyMatch(PropertyMapper::isBuildTime); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java index 83da1cc8202d..885dfd9224a4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/WildcardPropertyMapper.java @@ -9,10 +9,9 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.keycloak.config.LoggingOptions; import org.keycloak.config.Option; -import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; -import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; @@ -30,6 +29,7 @@ public class WildcardPropertyMapper extends PropertyMapper { private final String fromPrefix; private String toPrefix; private String toSuffix; + private Character replacementChar = null; public WildcardPropertyMapper(Option option, String to, BooleanSupplier enabled, String enabledWhen, ValueMapper mapper, String mapFrom, ValueMapper parentMapper, String paramLabel, boolean mask, BiConsumer, ConfigValue> validator, @@ -42,6 +42,10 @@ public WildcardPropertyMapper(Option option, String to, BooleanSupplier enabl throw new IllegalArgumentException("Invalid wildcard from format. Wildcard must be at the end of the option."); } + if (option == LoggingOptions.LOG_LEVEL_CATEGORY) { + replacementChar = '.'; + } + if (to != null) { if (!to.startsWith(NS_QUARKUS_PREFIX) && !to.startsWith(NS_KEYCLOAK_PREFIX)) { throw new IllegalArgumentException("Wildcards should map to Quarkus or Keycloak options (option '%s' mapped to '%s'). If not, PropertyMappers logic will need adjusted".formatted(option.getKey(), to)); @@ -119,7 +123,10 @@ public boolean matchesWildcardOptionName(String name) { public Optional getKcKeyForEnvKey(String envKey, String transformedKey) { if (transformedKey.startsWith(fromPrefix)) { - return Optional.ofNullable(getFrom(envKey.substring(fromPrefix.length()).toLowerCase().replace("_", "."))); + if (replacementChar != null) { + return Optional.ofNullable(getFrom(envKey.substring(fromPrefix.length()).toLowerCase().replace('_', replacementChar))); + } + return Optional.of(transformedKey); } return Optional.empty(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java index b3ca09965901..85d6a296f8db 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java @@ -21,8 +21,6 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.platform.Platform; -import org.keycloak.quarkus.runtime.Environment; -import org.keycloak.quarkus.runtime.cli.command.AbstractNonServerCommand; import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor; @@ -50,11 +48,6 @@ void onStartupEvent(@Observes StartupEvent event) { QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); platform.started(); startup(); - Environment.getParsedCommand().ifPresent(ac -> { - if (ac instanceof AbstractNonServerCommand) { - ((AbstractNonServerCommand)ac).onStart(this); - } - }); } void onShutdownEvent(@Observes ShutdownEvent event) { diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java index e4ce136cac8f..c4bec4e17910 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/cli/PicocliTest.java @@ -33,7 +33,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.List; import java.util.Map; import java.util.Properties; import java.util.function.Consumer; @@ -45,8 +44,10 @@ import org.keycloak.config.LoggingOptions; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.KeycloakMain; +import org.keycloak.quarkus.runtime.cli.command.AbstractCommand; import org.keycloak.quarkus.runtime.configuration.AbstractConfigurationTest; -import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider; import io.smallrye.config.SmallRyeConfig; import picocli.CommandLine; @@ -100,9 +101,10 @@ public void start() { } @Override - protected void initProfile(List cliArgs, String currentCommandName) { - super.initProfile(cliArgs, currentCommandName); - config = createConfig(); + public void initConfig(AbstractCommand command) { + KeycloakConfigSourceProvider.reload(); + super.initConfig(command); + config = Configuration.getConfig(); } @Override @@ -115,22 +117,43 @@ public void build() throws Throwable { NonRunningPicocli pseudoLaunch(String... args) { NonRunningPicocli nonRunningPicocli = new NonRunningPicocli(); - ConfigArgsConfigSource.setCliArgs(args); - nonRunningPicocli.config = createConfig(); KeycloakMain.main(args, nonRunningPicocli); return nonRunningPicocli; } + @Test + public void testUnbuiltHelp() { + NonRunningPicocli nonRunningPicocli = pseudoLaunch("bootstrap-admin"); + assertTrue(nonRunningPicocli.getErrString().contains("Missing required subcommand")); + } + + @Test + public void testProfileForHelp() { + NonRunningPicocli nonRunningPicocli = pseudoLaunch("-pf=dev", "bootstrap-admin", "-h"); + assertEquals("dev", nonRunningPicocli.config.getConfigValue("kc.profile").getValue()); + } + + @Test + public void testCleanStartDev() { + NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev"); + assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("WARN")); + assertFalse(nonRunningPicocli.getOutString(), nonRunningPicocli.getOutString().toUpperCase().contains("ERROR")); + } + @Test public void testNegativeArgument() { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertEquals("1h", nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); + assertEquals("1h", + nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue()); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--https-certificates-reload-period=-1"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertNull(nonRunningPicocli.config.getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); + assertNull(nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue()); } @Test @@ -140,10 +163,12 @@ public void testNegativeArgumentMgmtInterfaceCertReload() { assertEquals("1h", nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue()); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--https-management-certificates-reload-period=-1"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertNull(nonRunningPicocli.config.getConfigValue("quarkus.management.ssl.certificate.reload-period").getValue()); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--https-certificates-reload-period=5m"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertEquals("5m", @@ -287,7 +312,7 @@ public void spiRuntimeWarnWithBuild() { @Test public void failBuildDev() { - NonRunningPicocli nonRunningPicocli = pseudoLaunch("--profile=dev", "build"); + NonRunningPicocli nonRunningPicocli = pseudoLaunch("--profile=dev", "build", "--verbose"); assertThat(nonRunningPicocli.getErrString(), containsString("You can not 'build' the server in development mode.")); assertEquals(CommandLine.ExitCode.SOFTWARE, nonRunningPicocli.exitCode); } @@ -341,7 +366,7 @@ private NonRunningPicocli build(Consumer outChecker, String... args) { Environment.setRebuildCheck(); // auto-build } NonRunningPicocli nonRunningPicocli = pseudoLaunch(args); - assertTrue(nonRunningPicocli.reaug); + assertTrue(nonRunningPicocli.getErrString(), nonRunningPicocli.reaug); assertEquals(nonRunningPicocli.getErrString(), CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); outChecker.accept(nonRunningPicocli.getOutString()); onAfter(); @@ -401,7 +426,7 @@ public void testNoReaugFromDevToDevExport() { @Test public void testReaugFromDevToProdExport() { - build("start-dev"); + build("start-dev", "-v"); Environment.setRebuildCheck(); // will be reset by the system properties logic NonRunningPicocli nonRunningPicocli = pseudoLaunch("export", "--db=dev-file", "--file=file"); @@ -429,7 +454,7 @@ public void fastStartOptimizedSucceeds() { System.setProperty("kc.hostname-strict", "false"); NonRunningPicocli nonRunningPicocli = pseudoLaunch("start", "--optimized"); - assertEquals(Integer.MAX_VALUE, nonRunningPicocli.exitCode); // "running" state + assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); } @Test @@ -478,10 +503,12 @@ public void syslogMaxLengthMemorySize() { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-max-length=60k"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertEquals("60k", nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.max-length").getValue()); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.max-length").getValue(), nullValue()); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-max-length=wrong"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); @@ -496,18 +523,22 @@ public void syslogCountingFraming() { assertThat(nonRunningPicocli.getErrString(), containsString( "Invalid value for option '--log-syslog-counting-framing': TRUE. Expected values are: true, false, protocol-dependent")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=true"); assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK)); assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("true")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=false"); assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK)); assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("false")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-protocol=ssl-tcp", "--log-syslog-counting-framing=protocol-dependent"); assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.OK)); assertThat(nonRunningPicocli.config.getConfigValue("quarkus.log.syslog.use-counting-framing").getValue(), is("true")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-counting-framing=wrong"); assertThat(nonRunningPicocli.exitCode, is(CommandLine.ExitCode.USAGE)); assertThat(nonRunningPicocli.getErrString(), containsString( @@ -569,6 +600,7 @@ public void logConsoleJsonFormat() { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=invalid"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-console-json-format': invalid. Expected values are: default, ecs")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=ecs"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); @@ -586,6 +618,7 @@ public void logFileJsonFormat() { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=invalid"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-console-json-format': invalid. Expected values are: default, ecs")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log-console-output=json", "--log-console-json-format=ecs"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); @@ -603,6 +636,7 @@ public void logSyslogJsonFormat() { NonRunningPicocli nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-output=json", "--log-syslog-json-format=invalid"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-syslog-json-format': invalid. Expected values are: default, ecs")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=syslog", "--log-syslog-output=json", "--log-syslog-json-format=ecs"); assertEquals(CommandLine.ExitCode.OK, nonRunningPicocli.exitCode); @@ -674,10 +708,12 @@ public void logAsyncDisabledParent() { assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-console-async'. Available only when Console log handler is activated")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=console", "--log-file-async=true"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-file-async'. Available only when File log handler is activated")); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=file", "--log-syslog-async=true"); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-syslog-async'. Available only when Syslog is activated")); @@ -727,6 +763,7 @@ private void assertLogAsyncHandlerDisabledOptions(LoggingOptions.Handler logHand assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Invalid value for option '--log-%s-async-queue-length': 'invalid' is not an int".formatted(logHandlerOptionsName))); + onAfter(); nonRunningPicocli = pseudoLaunch("start-dev", "--log=%s".formatted(logHandlerName), "--log-%s-async-queue-length=768".formatted(logHandlerOptionsName)); assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); assertThat(nonRunningPicocli.getErrString(), containsString("Disabled option: '--log-%s-async-queue-length'. Available only when %s is activated and asynchronous logging is enabled".formatted(logHandlerOptionsName, logHandlerFullName))); @@ -796,6 +833,13 @@ public void datasourcesNotAllowedChar(){ assertThat(nonRunningPicocli.getErrString(), containsString("Unknown option: '--db-kind-'")); } + @Test + public void updateCommandValidation(){ + NonRunningPicocli nonRunningPicocli = pseudoLaunch("update-compatibility","check"); + assertEquals(CommandLine.ExitCode.USAGE, nonRunningPicocli.exitCode); + assertThat(nonRunningPicocli.getErrString(), containsString("Missing required argument: --file")); + } + @Test public void errorSpiBuildtimeChanged() { putEnvVar("KC_SPI_EVENTS_LISTENER__PROVIDER", "jboss-logging"); diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java index ff44dcd6c253..c9d0bb86422d 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/AbstractConfigurationTest.java @@ -63,7 +63,6 @@ public static void removeEnvVar(String name) { public static void setSystemProperty(String key, String value, Runnable runnable) { System.setProperty(key, value); - createConfig(); try { runnable.run(); } finally { diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java index 39eb6af72bc9..f9c25c4fb63b 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/ConfigurationTest.java @@ -45,7 +45,6 @@ import org.junit.Test; import org.keycloak.Config; import org.keycloak.config.CachingOptions; -import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory; @@ -355,6 +354,24 @@ public void testDatabaseProperties() { config = createConfig(); assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue()); assertEquals("test-schema", config.getConfigValue("kc.db-schema").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres"); + config = createConfig(); + assertEquals("primary", config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url-properties=?targetServerType=any"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + assertEquals("jdbc:postgresql://localhost:5432/keycloak?targetServerType=any", config.getConfigValue("quarkus.datasource.jdbc.url").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-driver=software.amazon.jdbc.Driver"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); + + ConfigArgsConfigSource.setCliArgs("--db=postgres", "--db-url=jdbc:postgresql://localhost:5432/keycloak?targetServerType=any"); + config = createConfig(); + assertNull(config.getConfigValue("quarkus.datasource.jdbc.additional-jdbc-properties.targetServerType").getValue()); } // KEYCLOAK-15632 @@ -504,11 +521,23 @@ public void testKeystoreConfigSourcePropertyMapping() { @Test public void testReloadPeriod() { ConfigArgsConfigSource.setCliArgs(""); - assertEquals("1h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); + initConfig(); + assertExternalConfig(Map.of( + "quarkus.http.ssl.certificate.reload-period", "1h", + "quarkus.management.ssl.certificate.reload-period", "1h" + )); + ConfigArgsConfigSource.setCliArgs("--https-certificates-reload-period=-1"); - assertNull(createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); + initConfig(); + assertExternalConfigNull("quarkus.http.ssl.certificate.reload-period"); + assertExternalConfigNull("quarkus.management.ssl.certificate.reload-period"); + ConfigArgsConfigSource.setCliArgs("--https-certificates-reload-period=2h"); - assertEquals("2h", createConfig().getConfigValue("quarkus.http.ssl.certificate.reload-period").getValue()); + initConfig(); + assertExternalConfig(Map.of( + "quarkus.http.ssl.certificate.reload-period", "2h", + "quarkus.management.ssl.certificate.reload-period", "2h" + )); } @Test diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java index b593412a04ef..b289dcf9231b 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/DatasourcesConfigurationTest.java @@ -8,14 +8,14 @@ import org.hibernate.dialect.PostgreSQLDialect; import org.junit.Test; import org.keycloak.quarkus.runtime.Environment; -import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource; -import org.keycloak.quarkus.runtime.configuration.Configuration; import org.mariadb.jdbc.MariaDbDataSource; import org.postgresql.xa.PGXADataSource; import java.util.Map; +import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -387,4 +387,53 @@ public void poolSizeInherit() { "quarkus.datasource.\"users\".jdbc.max-size", "115" )); } + + @Test + public void envVarsHandling() { + putEnvVars(Map.of( + "KC_DB_KIND_USER_STORE", "postgres", + "KC_DB_URL_FULL_USER_STORE", "jdbc:postgresql://localhost/KEYCLOAK", + "KC_DB_USERNAME_USER_STORE", "my-username", + "KC_DB_KIND_MY_STORE", "mariadb" + )); + initConfig(); + + assertConfig(Map.of( + "db-kind-user-store", "postgres", + "db-url-full-user-store", "jdbc:postgresql://localhost/KEYCLOAK", + "db-username-user-store", "my-username", + "db-kind-my-store", "mariadb" + )); + + assertExternalConfig(Map.of( + "quarkus.datasource.\"user-store\".db-kind", "postgresql", + "quarkus.datasource.\"user-store\".jdbc.url", "jdbc:postgresql://localhost/KEYCLOAK", + "quarkus.datasource.\"user-store\".username", "my-username", + "quarkus.datasource.\"my-store\".db-kind", "mariadb" + )); + + assertThat(Configuration.getPropertyNames(), hasItem("quarkus.datasource.\"my-store\".db-kind")); + assertThat(Configuration.getPropertyNames(), not(hasItem("quarkus.datasource.\"my.store\".db-kind"))); + } + + @Test + public void envVarsSpecialChars() { + putEnvVars(Map.of( + "KC_USER_STORE_DB_KIND", "mariadb", + "KCKEY_USER_STORE_DB_KIND", "db-kind-user_store$something", + "KC_CLIENT_STORE_PW", "password", + "KCKEY_CLIENT_STORE_PW", "db-password-client.store_123" + )); + initConfig(); + + assertConfig(Map.of( + "db-kind-user_store$something", "mariadb", + "db-password-client.store_123", "password" + )); + + assertExternalConfig(Map.of( + "quarkus.datasource.\"user_store$something\".db-kind", "mariadb", + "quarkus.datasource.\"client.store_123\".password", "password" + )); + } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java index 29907a834a16..4e908278759a 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/BuildAndStartDistTest.java @@ -61,7 +61,7 @@ void testBuildAndStart(KeycloakDistribution dist) { cliResult.assertBuild(); cliResult = rawDist.run("start", OPTIMIZED_BUILD_OPTION_LONG); cliResult.assertNoBuild(); - assertTrue(cliResult.getErrorOutput().isBlank()); + assertTrue(cliResult.getErrorOutput().isBlank(), cliResult.getErrorOutput()); // running start without optimized flag should not cause a build cliResult = rawDist.run("start"); cliResult.assertNoBuild(); diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java index f9bcce729e0d..1b5f9fff099d 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/ClusterConfigDistTest.java @@ -47,6 +47,47 @@ void changeClusterSetting(CLIResult result) { result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); } + @Test + @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=127.0.0.1","--cache-embedded-network-bind-port=7801", "--cache-embedded-network-external-address=127.0.0.2", "--cache-embedded-network-external-port=7802"}) + void changeBindAndExternalAddress(CLIResult result) { + result.assertClusteredCache(); + result.assertMessage("physical addresses are `[127.0.0.2:7802]`"); + result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); + } + + @Test + @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=127.0.0.1", "-Djgroups.bind.address=127.0.0.2", "-Djgroups.bind_addr=127.0.0.3"}) + void testJGroupsBindAddressPropertyAlsoExists(CLIResult result) { + result.assertClusteredCache(); + result.assertMessage("Conflicting system property 'jgroups.bind.address' and CLI arg 'cache-embedded-network-bind-address' set, utilising CLI value '127.0.0.1'"); + result.assertMessage("Conflicting system property 'jgroups.bind_addr' and CLI arg 'cache-embedded-network-bind-address' set, utilising CLI value '127.0.0.1'"); + result.assertMessage("physical addresses are `[127.0.0.1:7800]`"); + result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); + } + + @Test + @Launch({ "start-dev", "--cache=ispn", "-Djgroups.bind.address=127.0.0.2", "-Djgroups.bind.port=7801"}) + void testJGroupsBindAddressProperty(CLIResult result) { + result.assertClusteredCache(); + result.assertMessage("physical addresses are `[127.0.0.2:7801]`"); + result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); + } + + @Test + @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=match-address:127.0.0.*"}) + void testBindSiteMatches(CLIResult result) { + result.assertClusteredCache(); + result.assertMessage("physical addresses are `[127.0.0."); + result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); + } + + @Test + @Launch({ "start-dev", "--cache=ispn", "--cache-embedded-network-bind-address=SITE_LOCAL"}) + void testBindSiteLocal(CLIResult result) { + result.assertClusteredCache(); + result.assertMessage("ISPN000078: Starting JGroups channel `ISPN` with stack `jdbc-ping`"); + } + @Test @Launch({ "start-dev", "--cache=ispn", "--cache-stack=jdbc-ping-udp"}) void testJdbcPingTCP(CLIResult result) { diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java index ac49cc6497d8..aaf42eaa5b97 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/cli/dist/StartCommandDistTest.java @@ -96,7 +96,7 @@ void noErrorSpiBuildtimeNotChanged(KeycloakDistribution dist) { @Test @Launch({ "--profile=dev", "start", "--db=dev-file" }) void failUsingDevProfile(CLIResult cliResult) { - assertTrue(cliResult.getErrorOutput().contains("ERROR: You can not 'start' the server in development mode. Please re-build the server first, using 'kc.sh build' for the default production mode."), + assertTrue(cliResult.getErrorOutput().contains("You can not 'start' the server in development mode. Please re-build the server first, using 'kc.sh build' for the default production mode."), () -> "The Output:\n" + cliResult.getErrorOutput() + "doesn't contains the expected string."); } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java index 14ab29933892..4bfb3b2d94c4 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/dist/DatasourcesDistTest.java @@ -6,6 +6,7 @@ import org.keycloak.it.junit5.extension.CLIResult; import org.keycloak.it.junit5.extension.DistributionTest; import org.keycloak.it.junit5.extension.RawDistOnly; +import org.keycloak.it.junit5.extension.WithEnvVars; import picocli.CommandLine; @DistributionTest @@ -27,4 +28,13 @@ public void multipleDatasourcesPrint(CLIResult result) { result.assertMessage("Multiple datasources are specified: clients, , users"); result.assertBuild(); } + + @Test + @WithEnvVars({"KC_DB_KIND_USERS", "postgres", "KC_DB_KIND_MY_AWESOME_CLIENTS", "mariadb"}) + @Launch({"build"}) + public void specifiedViaEnvVars(CLIResult result) { + result.assertMessage("Multiple datasources are specified: , my-awesome-clients, users"); + result.assertBuild(); + } + } diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt index 44f8cf381d42..2820e70a4005 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminService.approved.txt @@ -70,52 +70,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt index 1090c8783614..7f4ab78b4430 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBootstrapAdminUser.approved.txt @@ -72,52 +72,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt index 970de3f26d0a..6ec640018de2 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testBuildHelp.approved.txt @@ -26,17 +26,17 @@ Database: --db-driver The fully qualified class name of the JDBC driver. If not set, a default driver is set accordingly to the chosen database. -Database - additional datasources: +Database - additional datasources (Preview): --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt index 91f0ca7f7d1a..bb65228053de 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelp.approved.txt @@ -65,52 +65,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt index c89fa6f5bf27..4bb031a57d36 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testExportHelpAll.approved.txt @@ -65,52 +65,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt index 7dea734f83c6..703b9577d51f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelp.approved.txt @@ -65,52 +65,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt index bb432d60497d..e181b058b81b 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testImportHelpAll.approved.txt @@ -65,52 +65,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt index e1155e681c28..47906d66bd67 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelp.approved.txt @@ -107,52 +107,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt index 7b2707a86fd9..d52575c04edf 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartDevHelpAll.approved.txt @@ -61,6 +61,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address

+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -158,52 +174,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt index 07f8f965ff22..5d966ac23e0d 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelp.approved.txt @@ -62,6 +62,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -139,52 +155,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt index d4fc1a3b4789..d1bf63ebfcab 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartHelpAll.approved.txt @@ -62,6 +62,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -159,52 +175,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt index 7b917a7c23af..e54867f28a7f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelp.approved.txt @@ -62,6 +62,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -133,43 +149,45 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Hostname v2: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt index 33a0e7a0160e..5305f645d320 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testStartOptimizedHelpAll.approved.txt @@ -62,6 +62,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -153,43 +169,45 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Hostname v2: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt index 3b5cf519321f..7ab7f2bbd818 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelp.approved.txt @@ -61,6 +61,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -138,52 +154,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt index 1e499462cd35..4b24fbec8e4d 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityCheckHelpAll.approved.txt @@ -61,6 +61,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -158,52 +174,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt index 655b61161cb1..22987d948d0c 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelp.approved.txt @@ -59,6 +59,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -136,52 +152,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt index f1cee186f270..0ea5dfc10bb6 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/dist/approvals/cli/help/HelpCommandDistTest.testUpdateCompatibilityMetadataHelpAll.approved.txt @@ -59,6 +59,22 @@ Cache: --cache-embedded-mtls-trust-store-password The password to access the Truststore. Available only when property 'cache-embedded-mtls-enabled' is enabled. +--cache-embedded-network-bind-address
+ IP address used by clustering transport. By default, SITE_LOCAL is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-bind-port + The Port the clustering transport will bind to. By default, port 7800 is used. + Available only when Infinispan clustered embedded is enabled. +--cache-embedded-network-external-address
+ IP address that other instances in the cluster should use to contact this + node. Set only if it is different to cache-embedded-network-bind-address, + for example when this instance is behind a firewall. Available only when + Infinispan clustered embedded is enabled. +--cache-embedded-network-external-port + Port that other instances in the cluster should use to contact this node. Set + only if it is different to cache-embedded-network-bind-port, for example + when this instance is behind a firewall Available only when Infinispan + clustered embedded is enabled. --cache-embedded-offline-client-sessions-max-count The maximum number of entries that can be stored in-memory by the offlineClientSessions cache. Available only when embedded Infinispan @@ -156,52 +172,54 @@ Database: --db-username The username of the database user. -Database - additional datasources: +Database - additional datasources (Preview): --db-active- - Deactivate specific named datasource . Default: true. + Preview: Deactivate specific named datasource . Default: true. --db-driver- - Used for named . The fully qualified class name of the JDBC - driver. If not set, a default driver is set accordingly to the chosen + Preview: Used for named . The fully qualified class name of the + JDBC driver. If not set, a default driver is set accordingly to the chosen database. --db-kind- - Used for named . The database vendor. In production mode the - default value of 'dev-file' is deprecated, you should explicitly specify the - db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, mysql, - oracle, postgres. Default: dev-file. + Preview: Used for named . The database vendor. In production mode + the default value of 'dev-file' is deprecated, you should explicitly specify + the db instead. Possible values are: dev-file, dev-mem, mariadb, mssql, + mysql, oracle, postgres. Default: dev-file. --db-password- - Used for named . The password of the database user. + Preview: Used for named . The password of the database user. --db-pool-initial-size- - Used for named . The initial size of the connection pool. + Preview: Used for named . The initial size of the connection pool. --db-pool-max-size- - Used for named . The maximum size of the connection pool. Default: - 100. + Preview: Used for named . The maximum size of the connection pool. + Default: 100. --db-pool-min-size- - Used for named . The minimal size of the connection pool. + Preview: Used for named . The minimal size of the connection pool. --db-schema- - Used for named . The database schema to be used. + Preview: Used for named . The database schema to be used. --db-url-database- - Used for named . Sets the database name of the default JDBC URL of - the chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the database name of the default + JDBC URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-full- - Used for named . The full database JDBC URL. If not provided, a - default URL is set based on the selected database vendor. For instance, if - using 'postgres', the default JDBC URL would be 'jdbc:postgresql: - //localhost/keycloak'. + Preview: Used for named . The full database JDBC URL. If not + provided, a default URL is set based on the selected database vendor. For + instance, if using 'postgres', the default JDBC URL would be 'jdbc: + postgresql://localhost/keycloak'. --db-url-host- - Used for named . Sets the hostname of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the hostname of the default JDBC + URL of the chosen vendor. If the `db-url` option is set, this option is + ignored. --db-url-port- - Used for named . Sets the port of the default JDBC URL of the - chosen vendor. If the `db-url` option is set, this option is ignored. + Preview: Used for named . Sets the port of the default JDBC URL of + the chosen vendor. If the `db-url` option is set, this option is ignored. --db-url-properties- - Used for named . Sets the properties of the default JDBC URL of - the chosen vendor. Make sure to set the properties accordingly to the format - expected by the database vendor, as well as appending the right character at - the beginning of this property value. If the `db-url` option is set, this - option is ignored. + Preview: Used for named . Sets the properties of the default JDBC + URL of the chosen vendor. Make sure to set the properties accordingly to the + format expected by the database vendor, as well as appending the right + character at the beginning of this property value. If the `db-url` option is + set, this option is ignored. --db-username- - Used for named . The username of the database user. + Preview: Used for named . The username of the database user. Transaction: diff --git a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java index 1c41c1ac2a09..c5793a7d6c66 100644 --- a/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java +++ b/quarkus/tests/junit5/src/main/java/org/keycloak/it/utils/DockerKeycloakDistribution.java @@ -56,6 +56,8 @@ public void accept(OutputFrame t) { private static final Logger LOGGER = Logger.getLogger(DockerKeycloakDistribution.class); + public static final int STARTUP_TIMEOUT_SECONDS = 120; + private final boolean debug; private final boolean manualStop; private final int requestPort; @@ -102,7 +104,7 @@ private GenericContainer getKeycloakContainer() { .withEnv(envVars) .withExposedPorts(exposedPorts) .withStartupAttempts(1) - .withStartupTimeout(Duration.ofSeconds(120)) + .withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT_SECONDS)) .waitingFor(Wait.forListeningPorts(8080)); } diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java index cf630d108154..ff253d4f5ff8 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/BruteForceUsersResource.java @@ -18,6 +18,7 @@ import org.jboss.logging.Logger; import org.keycloak.admin.ui.rest.model.BruteUser; import org.keycloak.authorization.fgap.AdminPermissionsSchema; +import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.models.Constants; import org.keycloak.models.KeycloakSession; @@ -148,7 +149,7 @@ public final Stream searchUser(@QueryParam("search") String search, private Stream searchForUser(Map attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) { attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString()); - if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { Set groupIds = auth.groups().getGroupIdsWithViewPermission(); if (!groupIds.isEmpty()) { session.setAttribute(UserModel.GROUPS, groupIds); diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 0fb03847329d..811016ee380b 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -18,9 +18,11 @@ package org.keycloak.models.utils; import java.io.IOException; +import java.util.AbstractMap; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; @@ -635,25 +637,28 @@ public static void updateClientProtocolMappers(ClientRepresentation rep, ClientM public static void updateClientScopes(ClientRepresentation resourceRep, ClientModel client) { if (resourceRep.getDefaultClientScopes() != null || resourceRep.getOptionalClientScopes() != null) { - // First remove all default/built in client scopes - for (ClientScopeModel clientScope : client.getClientScopes(true).values()) { - client.removeClientScope(clientScope); - } - - // First remove all default/built in client scopes - for (ClientScopeModel clientScope : client.getClientScopes(false).values()) { - client.removeClientScope(clientScope); + // first collect all the desired scopes + LinkedHashMap allScopes = new LinkedHashMap(); + Optional.ofNullable(resourceRep.getOptionalClientScopes()).ifPresent(scopes -> scopes.forEach(scope -> allScopes.put(scope, false))); + Optional.ofNullable(resourceRep.getDefaultClientScopes()).ifPresent(scopes -> scopes.forEach(scope -> allScopes.put(scope, true))); + + // next determine what already exists + Map, ClientScopeModel> existing = new HashMap, ClientScopeModel>(); + client.getClientScopes(false).entrySet().stream().forEach(entry -> existing.put(new AbstractMap.SimpleEntry(entry.getKey(), false), entry.getValue())); + client.getClientScopes(true).entrySet().stream().forEach(entry -> existing.put(new AbstractMap.SimpleEntry(entry.getKey(), true), entry.getValue())); + + // remove anything that isn't desired - this includes client scopes that are toggling the default flag + for (Entry, ClientScopeModel> entry : existing.entrySet()) { + if (Optional.ofNullable(allScopes.get(entry.getKey().getKey())).filter(entry.getKey().getValue()::equals).isEmpty()) { + client.removeClientScope(entry.getValue()); + } } - } - if (resourceRep.getDefaultClientScopes() != null) { - for (String clientScopeName : resourceRep.getDefaultClientScopes()) { - addClientScopeToClient(client.getRealm(), client, clientScopeName, true); - } - } - if (resourceRep.getOptionalClientScopes() != null) { - for (String clientScopeName : resourceRep.getOptionalClientScopes()) { - addClientScopeToClient(client.getRealm(), client, clientScopeName, false); + // finally add in all the desired + for (Map.Entry entry : allScopes.entrySet()) { + if (!existing.containsKey(entry)) { + addClientScopeToClient(client.getRealm(), client, entry.getKey(), entry.getValue()); + } } } } diff --git a/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProvider.java index 358870206b52..f2ce8cf672ad 100755 --- a/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProvider.java @@ -27,6 +27,7 @@ import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.TokenExchangeContext; import java.io.IOException; @@ -91,6 +92,14 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { return identity; } + @Override + protected BrokeredIdentityContext exchangeExternalTokenV2Impl(TokenExchangeContext tokenExchangeContext) { + // Supporting only introspection-endpoint validation for now + validateExternalTokenWithIntrospectionEndpoint(tokenExchangeContext); + + return exchangeExternalUserInfoValidationOnly(tokenExchangeContext.getEvent(), tokenExchangeContext.getFormParams()); + } + private JsonNode fetchUserProfile(String accessToken) { String userInfoUrl = getConfig().getUserInfoUrl(); diff --git a/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProviderFactory.java index ba50d044895c..4de3e5960233 100755 --- a/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oauth/OAuth2IdentityProviderFactory.java @@ -90,6 +90,12 @@ public Map parseConfig(KeycloakSession session, String rawConfig config.setAuthorizationUrl(rep.getAuthorizationEndpoint()); config.setTokenUrl(rep.getTokenEndpoint()); config.setUserInfoUrl(rep.getUserinfoEndpoint()); + + // Introspection URL may or may not be available in the configuration. It is mentioned in RFC8414 , but not in the OIDC discovery specification. + // Hence some servers may not add it to their well-known responses + if (rep.getIntrospectionEndpoint() != null) { + config.setTokenIntrospectionUrl(rep.getIntrospectionEndpoint()); + } return config.getConfig(); } } diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index ddfc80e29392..129e6723bb56 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -56,19 +56,25 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.AccessTokenIntrospectionProviderFactory; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.TokenExchangeContext; import org.keycloak.protocol.oidc.TokenExchangeProvider; import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; +import org.keycloak.protocol.oidc.endpoints.TokenIntrospectionEndpoint; import org.keycloak.protocol.oidc.utils.PkceUtils; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.JsonWebToken; +import org.keycloak.representations.oidc.TokenMetadataRepresentation; import org.keycloak.services.ErrorPage; import org.keycloak.services.ErrorResponseException; import org.keycloak.services.Urls; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.RealmsResource; import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.urls.UrlType; import org.keycloak.utils.StringUtil; import org.keycloak.vault.VaultStringSecret; @@ -657,6 +663,7 @@ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuilder event, String subjectToken, String subjectTokenType) { event.detail("validation_method", "user info"); + SimpleHttp.Response response = null; int status = 0; try { @@ -754,6 +761,111 @@ protected BrokeredIdentityContext exchangeExternalTokenV2Impl(TokenExchangeConte throw new UnsupportedOperationException("Not yet supported to verify the external token of the identity provider " + getConfig().getAlias()); } + /** + * Called usually during external-internal token exchange for validation of external token, which is the token issued by the IDP. + * The validation of external token is done by calling OAuth2 introspection endpoint on the IDP side and validate if the response contains all the necessary claims + * and token is authorized for the token exchange (including validating of claims like aud from introspection response) + * + * @param tokenExchangeContext token exchange context with the external token (subject token) and other details related to token exchange + * @throws ErrorResponseException in case that validation failed for any reason + */ + protected void validateExternalTokenWithIntrospectionEndpoint(TokenExchangeContext tokenExchangeContext) { + EventBuilder event = tokenExchangeContext.getEvent(); + + TokenMetadataRepresentation tokenMetadata = sendTokenIntrospectionRequest(tokenExchangeContext.getParams().getSubjectToken(), event); + + boolean clientValid = false; + String tokenClientId = tokenMetadata.getClientId(); + List tokenAudiences = null; + if (tokenClientId != null && tokenClientId.equals(getConfig().getClientId())) { + // Consider external token valid if issued to same client, which was configured as the client on IDP side + clientValid = true; + } else if (tokenMetadata.getAudience() != null && tokenMetadata.getAudience().length > 0) { + tokenAudiences = Arrays.stream(tokenMetadata.getAudience()).toList(); + if (tokenAudiences.contains(getConfig().getClientId())) { + // Consider external token valid if client configured as the IDP client included in token audience + clientValid = true; + } else { + // Consider valid introspection also if token contains audience where URL is Keycloak server (either as issuer or as token-endpoint URL). + // Aligned with https://datatracker.ietf.org/doc/html/rfc7523#section-3 - point 3 + UriInfo frontendUriInfo = session.getContext().getUri(UrlType.FRONTEND); + UriInfo backendUriInfo = session.getContext().getUri(UrlType.BACKEND); + RealmModel realm = session.getContext().getRealm(); + String realmIssuer = Urls.realmIssuer(frontendUriInfo.getBaseUri(), realm.getName()); + String realmTokenUrl = RealmsResource.protocolUrl(backendUriInfo).clone() + .path(OIDCLoginProtocolService.class, "token") + .build(realm.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL).toString(); + if (tokenAudiences.contains(realmIssuer) || tokenAudiences.contains(realmTokenUrl)) { + clientValid = true; + } + } + } + if (!clientValid) { + logger.debugf("Token not authorized for token exchange. Token client Id: %s, Token audiences: %s", tokenClientId, tokenAudiences); + throwErrorResponse(event, Errors.INVALID_TOKEN, OAuthErrorException.INVALID_TOKEN, "Token not authorized for token exchange"); + } + } + + /** + * Send introspection request as specified in the OAuth2 token introspection specification. It requires + * + * @param idpAccessToken access token issued by the IDP + * @param event event builder + * @return token metadata in case that token introspection was successful and token is valid and active + * @throws ErrorResponseException in case that introspection response was not correct for any reason (other status than 200) or the token was not active + */ + protected TokenMetadataRepresentation sendTokenIntrospectionRequest(String idpAccessToken, EventBuilder event) { + String introspectionEndointUrl = getConfig().getTokenIntrospectionUrl(); + if (introspectionEndointUrl == null) { + throwErrorResponse(event, Errors.INVALID_CONFIG, OAuthErrorException.INVALID_REQUEST, "Introspection endpoint not configured for IDP"); + } + + try { + + // Supporting only access-tokens for now + SimpleHttp introspectionRequest = SimpleHttp.doPost(introspectionEndointUrl, session) + .param(TokenIntrospectionEndpoint.PARAM_TOKEN, idpAccessToken) + .param(TokenIntrospectionEndpoint.PARAM_TOKEN_TYPE_HINT, AccessTokenIntrospectionProviderFactory.ACCESS_TOKEN_TYPE); + introspectionRequest = authenticateTokenRequest(introspectionRequest); + + try (SimpleHttp.Response introspectionResponse = introspectionRequest.asResponse()) { + int status = introspectionResponse.getStatus(); + + if (status != 200) { + try { + logger.warnf("Failed to invoke introspection endpoint. Status: %d, Introspection response details: %s", status, introspectionResponse.asString()); + } catch (Exception ioe) { + logger.warnf("Failed to invoke introspection endpoint. Status: %d", status); + } + throwErrorResponse(event, Errors.INVALID_REQUEST, OAuthErrorException.INVALID_REQUEST, "Introspection endpoint call failure. Introspection response status: " + status); + } + + TokenMetadataRepresentation tokenMetadata = null; + try { + tokenMetadata = introspectionResponse.asJson(TokenMetadataRepresentation.class); + } catch (IOException e) { + throwErrorResponse(event, Errors.INVALID_TOKEN, OAuthErrorException.INVALID_TOKEN, "Invalid format of the introspection response"); + } + + if (!tokenMetadata.isActive()) { + throwErrorResponse(event, Errors.INVALID_TOKEN, OAuthErrorException.INVALID_TOKEN, "Token not active"); + } + + return tokenMetadata; + } + } catch (IOException e) { + logger.debug("Failed to invoke introspection endpoint", e); + throwErrorResponse(event, Errors.INVALID_TOKEN, OAuthErrorException.INVALID_TOKEN, "Failed to invoke introspection endpoint"); + return null; // Unreachable + } + } + + private void throwErrorResponse(EventBuilder event, String eventError, String oauthError, String errorDetails) { + event.detail(Details.REASON, errorDetails); + event.error(eventError); + throw new ErrorResponseException(oauthError, errorDetails, Response.Status.BAD_REQUEST); + } + protected BrokeredIdentityContext exchangeExternalUserInfoValidationOnly(EventBuilder event, MultivaluedMap params) { String subjectToken = params.getFirst(OAuth2Constants.SUBJECT_TOKEN); if (subjectToken == null) { diff --git a/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java index 611c1f19d701..ad03d43e0cc9 100644 --- a/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OAuth2IdentityProviderConfig.java @@ -34,6 +34,8 @@ public class OAuth2IdentityProviderConfig extends IdentityProviderModel { public static final String PKCE_ENABLED = "pkceEnabled"; public static final String PKCE_METHOD = "pkceMethod"; + public static final String TOKEN_ENDPOINT_URL = "tokenUrl"; + public static final String TOKEN_INTROSPECTION_URL = "tokenIntrospectionUrl"; public static final String JWT_X509_HEADERS_ENABLED = "jwtX509HeadersEnabled"; @@ -54,11 +56,11 @@ public void setAuthorizationUrl(String authorizationUrl) { } public String getTokenUrl() { - return getConfig().get("tokenUrl"); + return getConfig().get(TOKEN_ENDPOINT_URL); } public void setTokenUrl(String tokenUrl) { - getConfig().put("tokenUrl", tokenUrl); + getConfig().put(TOKEN_ENDPOINT_URL, tokenUrl); } public String getUserInfoUrl() { @@ -69,6 +71,14 @@ public void setUserInfoUrl(String userInfoUrl) { getConfig().put("userInfoUrl", userInfoUrl); } + public String getTokenIntrospectionUrl() { + return getConfig().get(TOKEN_INTROSPECTION_URL); + } + + public void setTokenIntrospectionUrl(String introspectionEndpointUrl) { + getConfig().put(TOKEN_INTROSPECTION_URL, introspectionEndpointUrl); + } + public String getClientId() { return getConfig().get("clientId"); } @@ -209,6 +219,7 @@ public void validate(RealmModel realm) { checkUrl(sslRequired, getAuthorizationUrl(), "authorization_url"); checkUrl(sslRequired, getTokenUrl(), "token_url"); checkUrl(sslRequired, getUserInfoUrl(), "userinfo_url"); + checkUrl(sslRequired, getTokenIntrospectionUrl(), "tokenIntrospection_url"); if (isPkceEnabled()) { String pkceMethod = getPkceMethod(); diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index eab1b532736a..84591004987c 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -53,6 +53,7 @@ import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.TokenExchangeContext; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; import org.keycloak.representations.JsonWebToken; @@ -956,6 +957,14 @@ protected BrokeredIdentityContext exchangeExternalTokenV1Impl(EventBuilder event } } + @Override + protected BrokeredIdentityContext exchangeExternalTokenV2Impl(TokenExchangeContext tokenExchangeContext) { + // Supporting only introspection-endpoint validation for now + validateExternalTokenWithIntrospectionEndpoint(tokenExchangeContext); + + return exchangeExternalUserInfoValidationOnly(tokenExchangeContext.getEvent(), tokenExchangeContext.getFormParams()); + } + @Override protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { UriBuilder uriBuilder = super.createAuthorizationUrl(request); diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java index f11fad4c6e1e..e6b32bc3964f 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java @@ -75,6 +75,12 @@ protected static Map parseOIDCConfig(KeycloakSession session, St config.setUseJwksUrl(true); config.setJwksUrl(rep.getJwksUri()); } + + // Introspection URL may or may not be available in the configuration. It is available in RFC8414 , but not in the OIDC discovery specification. + // Hence some servers may not add it to their well-known responses + if (rep.getIntrospectionEndpoint() != null) { + config.setTokenIntrospectionUrl(rep.getIntrospectionEndpoint()); + } return config.getConfig(); } diff --git a/services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java b/services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java index 17679728a6ca..a09c89aa7a27 100644 --- a/services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java +++ b/services/src/main/java/org/keycloak/cookie/DefaultCookieProvider.java @@ -77,7 +77,7 @@ public void set(CookieType cookieType, String value, int maxAge) { StringBuilder warning = new StringBuilder("Non-secure context detected; cookies are not secured, and will not be available in cross-origin POST requests."); String forwarded = session.getContext().getRequestHeaders().getHeaderString("Forwarded"); - String xForwarded = session.getContext().getRequestHeaders().getHeaderString("X-Forwarded"); + String xForwarded = session.getContext().getRequestHeaders().getHeaderString("X-Forwarded-Proto"); // if we are getting here then: // if passthrough or reencrypt, proxy-headers may be misconfigured diff --git a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java index 9cb3c71952b3..4efefe2d3eda 100644 --- a/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java +++ b/services/src/main/java/org/keycloak/jose/jws/DefaultTokenManager.java @@ -244,7 +244,7 @@ private String type(TokenCategory category) { switch (category) { case ACCESS: ClientModel client = session.getContext().getClient(); - return OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRfc9068AccessTokenHeaderType() + return client != null && OIDCAdvancedConfigWrapper.fromClientModel(client).isUseRfc9068AccessTokenHeaderType() ? TokenUtil.TOKEN_TYPE_JWT_ACCESS_TOKEN : "JWT"; case LOGOUT: diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java index 228c8ab7d6e6..28b72c70117a 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/issuance/OID4VCIssuerEndpoint.java @@ -408,7 +408,7 @@ public Response requestCredential( Object theCredential = getCredential(authResult, supportedCredentialConfiguration, credentialRequestVO); if (SUPPORTED_FORMATS.contains(requestedFormat)) { - responseVO.setCredential(theCredential); + responseVO.addCredential(theCredential); } else { throw new BadRequestException(getErrorResponse(ErrorType.UNSUPPORTED_CREDENTIAL_TYPE)); } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/CredentialResponse.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/CredentialResponse.java index 7d4d6f618702..d6f84d777d9a 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/CredentialResponse.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/CredentialResponse.java @@ -17,6 +17,9 @@ package org.keycloak.protocol.oid4vc.model; +import java.util.ArrayList; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -29,18 +32,37 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class CredentialResponse { - // concrete type depends on the format - private Object credential; + @JsonProperty("credentials") + private List credentials; + + @JsonProperty("transaction_id") + private String transactionId; @JsonProperty("notification_id") private String notificationId; - public Object getCredential() { - return credential; + public List getCredentials() { + return credentials; } - public CredentialResponse setCredential(Object credential) { - this.credential = credential; + public CredentialResponse setCredentials(List credentials) { + this.credentials = credentials; + return this; + } + + public void addCredential(Object credential) { + if (this.credentials == null) { + this.credentials = new ArrayList<>(); + } + this.credentials.add(new Credential().setCredential(credential)); + } + + public String getTransactionId() { + return transactionId; + } + + public CredentialResponse setTransactionId(String transactionId) { + this.transactionId = transactionId; return this; } @@ -52,4 +74,22 @@ public CredentialResponse setNotificationId(String notificationId) { this.notificationId = notificationId; return this; } + + /** + * Inner class to represent a single credential object within the credentials array. + */ + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class Credential { + @JsonProperty("credential") + private Object credential; + + public Object getCredential() { + return credential; + } + + public Credential setCredential(Object credential) { + this.credential = credential; + return this; + } + } } diff --git a/services/src/main/java/org/keycloak/protocol/oid4vc/model/PreAuthorizedCode.java b/services/src/main/java/org/keycloak/protocol/oid4vc/model/PreAuthorizedCode.java index 13a9c88bb813..54b42b5dd719 100644 --- a/services/src/main/java/org/keycloak/protocol/oid4vc/model/PreAuthorizedCode.java +++ b/services/src/main/java/org/keycloak/protocol/oid4vc/model/PreAuthorizedCode.java @@ -37,9 +37,6 @@ public class PreAuthorizedCode { @JsonProperty("tx_code") private TxCode txCode; - @JsonProperty("interval") - private long interval; - @JsonProperty("authorization_server") private String authorizationServer; @@ -61,15 +58,6 @@ public PreAuthorizedCode setTxCode(TxCode txCode) { return this; } - public long getInterval() { - return interval; - } - - public PreAuthorizedCode setInterval(long interval) { - this.interval = interval; - return this; - } - public String getAuthorizationServer() { return authorizationServer; } @@ -83,11 +71,11 @@ public PreAuthorizedCode setAuthorizationServer(String authorizationServer) { public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof PreAuthorizedCode that)) return false; - return getInterval() == that.getInterval() && Objects.equals(getPreAuthorizedCode(), that.getPreAuthorizedCode()) && Objects.equals(getTxCode(), that.getTxCode()) && Objects.equals(getAuthorizationServer(), that.getAuthorizationServer()); + return Objects.equals(getPreAuthorizedCode(), that.getPreAuthorizedCode()) && Objects.equals(getTxCode(), that.getTxCode()) && Objects.equals(getAuthorizationServer(), that.getAuthorizationServer()); } @Override public int hashCode() { - return Objects.hash(getPreAuthorizedCode(), getTxCode(), getInterval(), getAuthorizationServer()); + return Objects.hash(getPreAuthorizedCode(), getTxCode(), getAuthorizationServer()); } } diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java index c65e321cf381..fca2f0ffc8c0 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/AudienceProtocolMapper.java @@ -41,7 +41,7 @@ public class AudienceProtocolMapper extends AbstractOIDCProtocolMapper implement private static final String INCLUDED_CLIENT_AUDIENCE_LABEL = "included.client.audience.label"; private static final String INCLUDED_CLIENT_AUDIENCE_HELP_TEXT = "included.client.audience.tooltip"; - private static final String INCLUDED_CUSTOM_AUDIENCE = "included.custom.audience"; + public static final String INCLUDED_CUSTOM_AUDIENCE = "included.custom.audience"; private static final String INCLUDED_CUSTOM_AUDIENCE_LABEL = "included.custom.audience.label"; private static final String INCLUDED_CUSTOM_AUDIENCE_HELP_TEXT = "included.custom.audience.tooltip"; diff --git a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java index 5ced667f5964..1cf1d1288348 100755 --- a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java @@ -179,7 +179,7 @@ private Response createWelcomePage(String successMessage, String errorMessage) { Theme theme = getTheme(); if(Objects.isNull(theme)) { - logger.error("Theme is null please check the \"--spi-theme-default\" parameter"); + logger.error("Theme is null please check the \"--spi-theme--default\" parameter"); errorMessage = "The theme is null"; ResponseBuilder rb = Response.status(Status.BAD_REQUEST) .entity(errorMessage) diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index d9b79ed30170..65b0294d0c5e 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -312,6 +312,10 @@ public Stream getUsers( if (enabled != null) { attributes.put(UserModel.ENABLED, enabled.toString()); } + if (emailVerified != null) { + attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); + } + return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult, maxResults, false); } @@ -373,12 +377,17 @@ public Stream getUsers( * {@code email} or {@code username} those criteria are matched against their * respective fields on a user entity. Combined with a logical and. * - * @param search arbitrary search string for all the fields below. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and "foo" for exact search. - * @param last last name filter - * @param first first name filter - * @param email email filter - * @param username username filter + * @param search A String contained in username, first or last name, or email. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and "foo" for exact search. + * @param last A String contained in lastName, or the complete lastName, if param "exact" is true + * @param first A String contained in firstName, or the complete firstName, if param "exact" is true + * @param email A String contained in email, or the complete email, if param "exact" is true + * @param username A String contained in username, or the complete username, if param "exact" is true + * @param emailVerified whether the email has been verified + * @param idpAlias The alias of an Identity Provider linked to the user + * @param idpUserId The userId at an Identity Provider linked to the user * @param enabled Boolean representing if user is enabled or not + * @param exact Boolean which defines whether the params "last", "first", "email" and "username" must match exactly + * @param searchQuery A query to search for custom attributes, in the format 'key1:value2 key2:value2' * @return the number of users that match the given criteria */ @Path("count") @@ -397,32 +406,46 @@ public Stream getUsers( "2. If {@code search} is specified other criteria such as {@code last} will be ignored even though you set them. The {@code search} string will be matched against the first and last name, the username and the email of a user.

" + "3. If {@code search} is unspecified but any of {@code last}, {@code first}, {@code email} or {@code username} those criteria are matched against their respective fields on a user entity. Combined with a logical and.") public Integer getUsersCount( - @Parameter(description = "arbitrary search string for all the fields below. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.") @QueryParam("search") String search, - @Parameter(description = "last name filter") @QueryParam("lastName") String last, - @Parameter(description = "first name filter") @QueryParam("firstName") String first, - @Parameter(description = "email filter") @QueryParam("email") String email, - @QueryParam("emailVerified") Boolean emailVerified, - @Parameter(description = "username filter") @QueryParam("username") String username, + @Parameter(description = "A String contained in username, first or last name, or email. Default search behavior is prefix-based (e.g., foo or foo*). Use *foo* for infix search and \"foo\" for exact search.") @QueryParam("search") String search, + @Parameter(description = "A String contained in lastName, or the complete lastName, if param \"exact\" is true") @QueryParam("lastName") String last, + @Parameter(description = "A String contained in firstName, or the complete firstName, if param \"exact\" is true") @QueryParam("firstName") String first, + @Parameter(description = "A String contained in email, or the complete email, if param \"exact\" is true") @QueryParam("email") String email, + @Parameter(description = "A String contained in username, or the complete username, if param \"exact\" is true") @QueryParam("username") String username, + @Parameter(description = "whether the email has been verified") @QueryParam("emailVerified") Boolean emailVerified, + @Parameter(description = "The alias of an Identity Provider linked to the user") @QueryParam("idpAlias") String idpAlias, + @Parameter(description = "The userId at an Identity Provider linked to the user") @QueryParam("idpUserId") String idpUserId, @Parameter(description = "Boolean representing if user is enabled or not") @QueryParam("enabled") Boolean enabled, - @QueryParam("q") String searchQuery) { + @Parameter(description = "Boolean which defines whether the params \"last\", \"first\", \"email\" and \"username\" must match exactly") @QueryParam("exact") Boolean exact, + @Parameter(description = "A query to search for custom attributes, in the format 'key1:value2 key2:value2'") @QueryParam("q") String searchQuery) { UserPermissionEvaluator userPermissionEvaluator = auth.users(); userPermissionEvaluator.requireQuery(); Map searchAttributes = searchQuery == null ? Collections.emptyMap() : SearchQueryUtils.getFields(searchQuery); - if (search != null) { if (search.startsWith(SEARCH_ID_PARAMETER)) { UserModel userModel = session.users().getUserById(realm, search.substring(SEARCH_ID_PARAMETER.length()).trim()); return userModel != null && userPermissionEvaluator.canView(userModel) ? 1 : 0; - } else if (userPermissionEvaluator.canView()) { - return session.users().getUsersCount(realm, search.trim()); + } + + Map parameters = new HashMap<>(); + parameters.put(UserModel.SEARCH, search.trim()); + + if (enabled != null) { + parameters.put(UserModel.ENABLED, enabled.toString()); + } + if (emailVerified != null) { + parameters.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); + } + + if (userPermissionEvaluator.canView()) { + return session.users().getUsersCount(realm, parameters); } else { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { - return session.users().getUsersCount(realm, search.trim()); + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + return session.users().getUsersCount(realm, parameters, auth.groups().getGroupIdsWithViewPermission()); } else { - return session.users().getUsersCount(realm, search.trim(), auth.groups().getGroupIdsWithViewPermission()); + return session.users().getUsersCount(realm, parameters); } } } else if (last != null || first != null || email != null || username != null || emailVerified != null || enabled != null || !searchAttributes.isEmpty()) { @@ -442,27 +465,37 @@ public Integer getUsersCount( if (emailVerified != null) { parameters.put(UserModel.EMAIL_VERIFIED, emailVerified.toString()); } + if (idpAlias != null) { + parameters.put(UserModel.IDP_ALIAS, idpAlias); + } + if (idpUserId != null) { + parameters.put(UserModel.IDP_USER_ID, idpUserId); + } if (enabled != null) { parameters.put(UserModel.ENABLED, enabled.toString()); } + if (exact != null) { + parameters.put(UserModel.EXACT, exact.toString()); + } parameters.putAll(searchAttributes); if (userPermissionEvaluator.canView()) { return session.users().getUsersCount(realm, parameters); } else { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { - return session.users().getUsersCount(realm, parameters); - } else { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { return session.users().getUsersCount(realm, parameters, auth.groups().getGroupIdsWithViewPermission()); + } else { + return session.users().getUsersCount(realm, parameters); } } } else if (userPermissionEvaluator.canView()) { return session.users().getUsersCount(realm); } else { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { + return session.users().getUsersCount(realm, auth.groups().getGroupIdsWithViewPermission()); + } else { return session.users().getUsersCount(realm); } - return session.users().getUsersCount(realm, auth.groups().getGroupIdsWithViewPermission()); } } @@ -480,7 +513,7 @@ public UserProfileResource userProfile() { private Stream searchForUser(Map attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) { attributes.put(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts.toString()); - if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)) { Set groupIds = auth.groups().getGroupIdsWithViewPermission(); if (!groupIds.isEmpty()) { session.setAttribute(UserModel.GROUPS, groupIds); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/fgap/AdminPermissions.java b/services/src/main/java/org/keycloak/services/resources/admin/fgap/AdminPermissions.java index 1e8598937643..912baa2bf1d6 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/fgap/AdminPermissions.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/fgap/AdminPermissions.java @@ -16,7 +16,6 @@ */ package org.keycloak.services.resources.admin.fgap; -import org.keycloak.authorization.fgap.AdminPermissionsSchema; import org.keycloak.common.Profile; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; @@ -36,28 +35,27 @@ public class AdminPermissions { public static AdminPermissionEvaluator evaluator(KeycloakSession session, RealmModel realm, AdminAuth auth) { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) { return new MgmtPermissionsV2(session, realm, auth); } return new MgmtPermissions(session, realm, auth); } public static AdminPermissionEvaluator evaluator(KeycloakSession session, RealmModel realm, RealmModel adminsRealm, UserModel admin) { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) { return new MgmtPermissionsV2(session, adminsRealm, admin); } return new MgmtPermissions(session, realm, adminsRealm, admin); } public static RealmsPermissionEvaluator realms(KeycloakSession session, AdminAuth auth) { - RealmModel realm = session.getContext().getRealm(); - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) { return new MgmtPermissionsV2(session, auth); } return new MgmtPermissions(session, auth); } public static AdminPermissionManagement management(KeycloakSession session, RealmModel realm) { - if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm)) { + if (Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2)) { return new MgmtPermissionsV2(session, realm); } return new MgmtPermissions(session, realm); diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index 8bd7ead2fdd4..fc26c1a4514a 100755 --- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Response; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; @@ -70,8 +71,8 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { private void verifyToken(String accessToken) throws IOException { JsonNode response = SimpleHttp.doGet(DEBUG_TOKEN_URL, session) + .header(HttpHeaders.AUTHORIZATION, "Bearer " + getConfig().getClientId() + "|" + getConfig().getClientSecret()) .param("input_token", accessToken) - .param(OAuth2Constants.ACCESS_TOKEN, getConfig().getClientId() + "|" + getConfig().getClientSecret()) .asJson(); JsonNode errorNode = response.get("error"); diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index 85d9fd5e1f54..1aba16d4d15d 100755 --- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -19,7 +19,13 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.ws.rs.core.Response; + +import java.io.IOException; import java.util.Iterator; +import java.util.Map; + +import org.keycloak.OAuth2Constants; +import org.keycloak.OAuthErrorException; import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider; import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig; import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper; @@ -29,6 +35,9 @@ import org.keycloak.broker.social.SocialIdentityProvider; import org.keycloak.events.EventBuilder; import org.keycloak.models.KeycloakSession; +import org.keycloak.protocol.oidc.TokenExchangeContext; +import org.keycloak.services.ErrorResponseException; +import org.keycloak.util.BasicAuthHelper; /** * @author Stian Thorgersen @@ -48,8 +57,10 @@ public class GitHubIdentityProvider extends AbstractOAuth2IdentityProvider imple public static final String TOKEN_URL = DEFAULT_TOKEN_URL; public static final String DEFAULT_API_URL = "https://api.github.com"; + public static final String APPLICATIONS_FRAGMENT = "/applications"; public static final String PROFILE_FRAGMENT = "/user"; public static final String EMAIL_FRAGMENT = "/user/emails"; + public static final String DEFAULT_APPLICATIONS_URL = DEFAULT_API_URL + APPLICATIONS_FRAGMENT; public static final String DEFAULT_PROFILE_URL = DEFAULT_API_URL + PROFILE_FRAGMENT; public static final String DEFAULT_EMAIL_URL = DEFAULT_API_URL + EMAIL_FRAGMENT; /** @deprecated Use {@link #DEFAULT_PROFILE_URL} instead. */ @@ -153,7 +164,6 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { if (user.getEmail() == null) { user.setEmail(searchEmail(accessToken)); } - return user; } catch (Exception e) { throw new IdentityBrokerException("Profile could not be retrieved from the github endpoint", e); @@ -190,7 +200,45 @@ private String searchEmail(String accessToken) { } } - @Override + private void verifyToken(String accessToken) throws IOException { + String tokenUrl = DEFAULT_APPLICATIONS_URL + "/" + getConfig().getClientId() + "/token"; + SimpleHttp.Response response = SimpleHttp.doPost(tokenUrl, session) + .header("Authorization", BasicAuthHelper.createHeader(getConfig().getClientId(), getConfig().getClientSecret())) + .json(Map.of("access_token", accessToken)).asResponse(); + + JsonNode jsonNodeResponse = response.asJson(); + if (response.getStatus() != 200) { + String errorMessage = getJsonProperty(jsonNodeResponse, "message"); + throw new RuntimeException("Error message: " + errorMessage); + } + + JsonNode appNode = jsonNodeResponse.get("app"); + if (appNode == null || appNode.isNull()) { + throw new RuntimeException("Invalid token check response: 'app' field is missing."); + } + + String clientId = getJsonProperty(appNode, "client_id"); + if (!getConfig().getClientId().equals(clientId)) { + throw new RuntimeException("Client ID does not match the client_id in the access token check response."); + } + } + + @Override + protected BrokeredIdentityContext exchangeExternalTokenV2Impl(TokenExchangeContext tokenExchangeContext) { + String subjectToken = tokenExchangeContext.getFormParams().getFirst(OAuth2Constants.SUBJECT_TOKEN); + if (subjectToken == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "token not set", Response.Status.BAD_REQUEST); + } + try { + verifyToken(subjectToken); + return doGetFederatedIdentity(subjectToken); + } + catch (Exception e) { + throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, e.getMessage(), Response.Status.BAD_REQUEST); + } + } + + @Override protected String getDefaultScopes() { return DEFAULT_SCOPE; } diff --git a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java index 47c8cffc9607..2cd50ad63b0c 100644 --- a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java +++ b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java @@ -19,17 +19,21 @@ import java.util.Arrays; import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import org.infinispan.server.test.core.CountdownLatchLoggingConsumer; import org.jboss.logging.Logger; import org.keycloak.it.utils.DockerKeycloakDistribution; import org.keycloak.testframework.clustering.LoadBalancer; -import org.keycloak.testframework.database.JBossLogConsumer; +import org.keycloak.testframework.logging.JBossLogConsumer; import org.testcontainers.images.RemoteDockerImage; import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.LazyFuture; public class ClusteredKeycloakServer implements KeycloakServer { + private static final String CLUSTER_VIEW_REGEX = ".*ISPN000093.*(?<=\\()(%1$d)(?=\\)).*|.*ISPN000094.*(?<=\\()(%1$d)(?=\\)).*"; private static final boolean MANUAL_STOP = true; private static final int REQUEST_PORT = 8080; private static final int MANAGEMENT_PORT = 9000; @@ -49,25 +53,39 @@ public ClusteredKeycloakServer(int mumServers, String images) { @Override public void start(KeycloakServerConfigBuilder configBuilder) { + int numServers = containers.length; + CountdownLatchLoggingConsumer clusterLatch = new CountdownLatchLoggingConsumer(numServers, String.format(CLUSTER_VIEW_REGEX, numServers)); String[] imagePeServer = null; if (images == null || images.isEmpty() || (imagePeServer = images.split(",")).length == 1) { - startContainersWithSameImage(configBuilder, imagePeServer == null ? SNAPSHOT_IMAGE : imagePeServer[0]); + startContainersWithSameImage(configBuilder, imagePeServer == null ? SNAPSHOT_IMAGE : imagePeServer[0], clusterLatch); } else { - startContainersWithMixedImage(configBuilder, imagePeServer); + startContainersWithMixedImage(configBuilder, imagePeServer, clusterLatch); + } + + try { + clusterLatch.await((long) numServers * DockerKeycloakDistribution.STARTUP_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (TimeoutException e) { + throw new RuntimeException("Expected %d cluster members".formatted(numServers), e); } } - private void startContainersWithMixedImage(KeycloakServerConfigBuilder configBuilder, String[] imagePeServer) { + private void startContainersWithMixedImage(KeycloakServerConfigBuilder configBuilder, String[] imagePeServer, CountdownLatchLoggingConsumer clusterLatch) { assert imagePeServer != null; if (containers.length != imagePeServer.length) { throw new IllegalArgumentException("The number of containers and the number of images must match"); } + int[] exposedPorts = new int[]{REQUEST_PORT, MANAGEMENT_PORT}; LazyFuture snapshotImage = null; for (int i = 0; i < containers.length; ++i) { LazyFuture resolvedImage; if (SNAPSHOT_IMAGE.equals(imagePeServer[i])) { if (snapshotImage == null) { + // Required otherwise we will receive an "Incorrect state of migration" error preventing startup + configBuilder.option("spi-datastore--legacy--allow-migrate-existing-database-to-snapshot", "true"); snapshotImage = defaultImage(); } resolvedImage = snapshotImage; @@ -79,12 +97,12 @@ private void startContainersWithMixedImage(KeycloakServerConfigBuilder configBui copyProvidersAndConfigs(container, configBuilder); - container.setCustomLogConsumer(new JBossLogConsumer(Logger.getLogger("managed.keycloak." + i))); + configureLogConsumers(container, i, clusterLatch); container.run(configBuilder.toArgs()); } } - private void startContainersWithSameImage(KeycloakServerConfigBuilder configBuilder, String image) { + private void startContainersWithSameImage(KeycloakServerConfigBuilder configBuilder, String image, CountdownLatchLoggingConsumer clusterLatch) { int[] exposedPorts = new int[]{REQUEST_PORT, MANAGEMENT_PORT}; LazyFuture imageFuture = image == null || SNAPSHOT_IMAGE.equals(image) ? defaultImage() : @@ -94,12 +112,16 @@ private void startContainersWithSameImage(KeycloakServerConfigBuilder configBuil containers[i] = container; copyProvidersAndConfigs(container, configBuilder); - - container.setCustomLogConsumer(new JBossLogConsumer(Logger.getLogger("managed.keycloak." + i))); + configureLogConsumers(container, i, clusterLatch); container.run(configBuilder.toArgs()); } } + private static void configureLogConsumers(DockerKeycloakDistribution container, int index, CountdownLatchLoggingConsumer clusterLatch) { + var logger = new JBossLogConsumer(Logger.getLogger("managed.keycloak." + index)); + container.setCustomLogConsumer(logger.andThen(clusterLatch)); + } + private void copyProvidersAndConfigs(DockerKeycloakDistribution container, KeycloakServerConfigBuilder configBuilder) { for (var dependency : configBuilder.toDependencies()) { container.copyProvider(dependency.getGroupId(), dependency.getArtifactId()); diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/database/AbstractContainerTestDatabase.java b/test-framework/core/src/main/java/org/keycloak/testframework/database/AbstractContainerTestDatabase.java index 37024022e6f6..c8bc957566d2 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/database/AbstractContainerTestDatabase.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/database/AbstractContainerTestDatabase.java @@ -2,6 +2,7 @@ import org.jboss.logging.Logger; import org.keycloak.testframework.config.Config; +import org.keycloak.testframework.logging.JBossLogConsumer; import org.testcontainers.containers.JdbcDatabaseContainer; import java.io.IOException; diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/database/JBossLogConsumer.java b/test-framework/core/src/main/java/org/keycloak/testframework/logging/JBossLogConsumer.java similarity index 94% rename from test-framework/core/src/main/java/org/keycloak/testframework/database/JBossLogConsumer.java rename to test-framework/core/src/main/java/org/keycloak/testframework/logging/JBossLogConsumer.java index 9d35ad99709c..12ea46ebfa34 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/database/JBossLogConsumer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/logging/JBossLogConsumer.java @@ -1,4 +1,4 @@ -package org.keycloak.testframework.database; +package org.keycloak.testframework.logging; import org.jboss.logging.Logger; import org.testcontainers.containers.output.OutputFrame; diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java index 410322fc968f..f91d8238f97d 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTest.java @@ -21,7 +21,6 @@ import org.hamcrest.Matchers; import org.jgroups.util.UUID; import org.junit.jupiter.api.Test; -import org.keycloak.admin.client.resource.AuthorizationResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.AdminRoles; import org.keycloak.models.Constants; @@ -41,11 +40,6 @@ import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.representations.idm.UserRepresentation; -import org.keycloak.representations.idm.authorization.PolicyRepresentation; -import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; -import org.keycloak.representations.idm.authorization.ResourceRepresentation; -import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; -import org.keycloak.representations.idm.authorization.ScopeRepresentation; import org.keycloak.services.resources.admin.AdminAuth.Resource; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; @@ -58,14 +52,11 @@ import org.keycloak.testsuite.util.IdentityProviderBuilder; import org.keycloak.testsuite.util.RoleBuilder; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static org.hamcrest.MatcherAssert.assertThat; -import static org.keycloak.services.resources.admin.AdminAuth.Resource.AUTHORIZATION; -import static org.keycloak.services.resources.admin.AdminAuth.Resource.CLIENT; /** * @author Stian Thorgersen @@ -297,75 +288,6 @@ public void clientInitialAccess() { invoke(realm -> realm.clientInitialAccess().delete("nosuch"), Resource.CLIENT, true); } - @Test - public void clientAuthorization() { - String fooAuthzClientUuid = ApiUtil.getCreatedId(managedRealm1.admin().clients().create(ClientConfigBuilder.create().clientId("foo-authz").build())); - ClientRepresentation foo = managedRealm1.admin().clients().get(fooAuthzClientUuid).toRepresentation(); - - invoke((realm, response) -> { - foo.setServiceAccountsEnabled(true); - foo.setAuthorizationServicesEnabled(true); - realm.clients().get(foo.getId()).update(foo); - }, CLIENT, true); - invoke(realm -> realm.clients().get(foo.getId()).authorization().getSettings(), AUTHORIZATION, false); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - ResourceServerRepresentation settings = authorization.getSettings(); - authorization.update(settings); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.resources().resources(); - }, AUTHORIZATION, false); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.scopes().scopes(); - }, AUTHORIZATION, false); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.policies().policies(); - }, AUTHORIZATION, false); - invoke((realm, response) -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - response.set(authorization.resources().create(new ResourceRepresentation("Test", Collections.emptySet()))); - }, AUTHORIZATION, true); - invoke((realm, response) -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - response.set(authorization.scopes().create(new ScopeRepresentation("Test"))); - }, AUTHORIZATION, true); - invoke((realm, response) -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); - representation.setName("Test PermissionsTest"); - representation.addResource("Default Resource"); - response.set(authorization.permissions().resource().create(representation)); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.resources().resource("nosuch").update(new ResourceRepresentation()); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.scopes().scope("nosuch").update(new ScopeRepresentation()); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.policies().policy("nosuch").update(new PolicyRepresentation()); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.resources().resource("nosuch").remove(); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.scopes().scope("nosuch").remove(); - }, AUTHORIZATION, true); - invoke(realm -> { - AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); - authorization.policies().policy("nosuch").remove(); - }, AUTHORIZATION, true); - } - @Test public void roles() { RoleRepresentation newRole = RoleBuilder.create().name("sample-role").build(); @@ -544,9 +466,6 @@ public void users() { invoke(realm -> realm.users().get(user.getId()).update(user), clients.get(AdminRoles.QUERY_CLIENTS), false); // users with query-user role should be able to query required actions so the user detail page can be rendered successfully when fine-grained permissions are enabled. invoke(realm -> realm.flows().getRequiredActions(), clients.get(AdminRoles.QUERY_USERS), true); - // users with query-user role should be able to query clients so the user detail page can be rendered successfully when fine-grained permissions are enabled. - // if the admin wants to restrict the clients that an user can see he can define permissions for these clients - invoke(realm -> clients.get(AdminRoles.QUERY_USERS).realm(REALM_NAME).clients().findAll(), clients.get(AdminRoles.QUERY_USERS), true); invoke(realm -> clients.get(AdminRoles.VIEW_USERS).realm(REALM_NAME).users().get(user.getId()).getConfiguredUserStorageCredentialTypes(), clients.get(AdminRoles.VIEW_USERS), true); } diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTestV1.java b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTestV1.java new file mode 100644 index 000000000000..7c4934d56b8a --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/admin/PermissionsTestV1.java @@ -0,0 +1,120 @@ +/* + * Copyright 2016 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.tests.admin; + +import org.junit.jupiter.api.Test; +import org.keycloak.admin.client.resource.AuthorizationResource; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.representations.idm.authorization.ResourceServerRepresentation; +import org.keycloak.representations.idm.authorization.ScopeRepresentation; +import org.keycloak.testframework.annotations.InjectRealm; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.realm.ClientConfigBuilder; +import org.keycloak.testframework.realm.ManagedRealm; +import org.keycloak.tests.admin.authz.fgap.KeycloakAdminPermissionsV1ServerConfig; +import org.keycloak.tests.utils.admin.ApiUtil; + +import java.util.Collections; + +import static org.keycloak.services.resources.admin.AdminAuth.Resource.AUTHORIZATION; +import static org.keycloak.services.resources.admin.AdminAuth.Resource.CLIENT; + +/** + * @author Stian Thorgersen + */ +@KeycloakIntegrationTest(config = KeycloakAdminPermissionsV1ServerConfig.class) +public class PermissionsTestV1 extends AbstractPermissionsTest { + + @InjectRealm(config = PermissionsTestRealmConfig1.class, ref = "realm1") + ManagedRealm managedRealm1; + + @InjectRealm(config = PermissionsTestRealmConfig2.class, ref = "realm2") + ManagedRealm managedRealm2; + + @Test + public void clientAuthorization() { + String fooAuthzClientUuid = ApiUtil.getCreatedId(managedRealm1.admin().clients().create(ClientConfigBuilder.create().clientId("foo-authz").build())); + ClientRepresentation foo = managedRealm1.admin().clients().get(fooAuthzClientUuid).toRepresentation(); + + invoke((realm, response) -> { + foo.setServiceAccountsEnabled(true); + foo.setAuthorizationServicesEnabled(true); + realm.clients().get(foo.getId()).update(foo); + }, CLIENT, true); + invoke(realm -> realm.clients().get(foo.getId()).authorization().getSettings(), AUTHORIZATION, false); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + ResourceServerRepresentation settings = authorization.getSettings(); + authorization.update(settings); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.resources().resources(); + }, AUTHORIZATION, false); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.scopes().scopes(); + }, AUTHORIZATION, false); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.policies().policies(); + }, AUTHORIZATION, false); + invoke((realm, response) -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + response.set(authorization.resources().create(new ResourceRepresentation("Test", Collections.emptySet()))); + }, AUTHORIZATION, true); + invoke((realm, response) -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + response.set(authorization.scopes().create(new ScopeRepresentation("Test"))); + }, AUTHORIZATION, true); + invoke((realm, response) -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); + representation.setName("Test PermissionsTest"); + representation.addResource("Default Resource"); + response.set(authorization.permissions().resource().create(representation)); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.resources().resource("nosuch").update(new ResourceRepresentation()); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.scopes().scope("nosuch").update(new ScopeRepresentation()); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.policies().policy("nosuch").update(new PolicyRepresentation()); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.resources().resource("nosuch").remove(); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.scopes().scope("nosuch").remove(); + }, AUTHORIZATION, true); + invoke(realm -> { + AuthorizationResource authorization = realm.clients().get(foo.getId()).authorization(); + authorization.policies().policy("nosuch").remove(); + }, AUTHORIZATION, true); + } +} diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/UsersTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/UsersTest.java index da01c4e42eff..7914174623c5 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/UsersTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/UsersTest.java @@ -19,12 +19,17 @@ import org.junit.jupiter.api.Test; import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserProvider; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testframework.annotations.InjectRealm; import org.keycloak.testframework.annotations.KeycloakIntegrationTest; import org.keycloak.testframework.injection.LifeCycle; import org.keycloak.testframework.realm.ManagedRealm; import org.keycloak.testframework.realm.UserConfigBuilder; +import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; +import org.keycloak.testframework.remote.runonserver.RunOnServerClient; import java.util.List; @@ -37,23 +42,17 @@ @KeycloakIntegrationTest public class UsersTest { + private static final String realmName = "default"; + @InjectRealm(lifecycle = LifeCycle.METHOD) ManagedRealm realm; - private void createUser(String username, String password, String firstName, String lastName, String email) { - UserRepresentation user = UserConfigBuilder.create() - .username(username) - .password(password) - .name(firstName, lastName) - .email(email) - .enabled(true) - .build(); - realm.admin().users().create(user); - } + @InjectRunOnServer(permittedPackages = {"org.keycloak.tests", "org.keycloak.admin"}) + RunOnServerClient runOnServer; @Test public void searchUserWithWildcards() { - createUser("User", "password", "firstName", "lastName", "user@example.com"); + createUser("User", "firstName", "lastName", "user@example.com"); assertThat(realm.admin().users().search("Use%", null, null), hasSize(0)); assertThat(realm.admin().users().search("Use_", null, null), hasSize(0)); @@ -65,14 +64,14 @@ public void searchUserWithWildcards() { @Test public void searchUserDefaultSettings() throws Exception { - createUser("User", "password", "firstName", "lastName", "user@example.com"); + createUser("User", "firstName", "lastName", "user@example.com"); assertCaseInsensitiveSearch(); } @Test public void searchUserMatchUsersCount() { - createUser("john.doe", "password", "John", "Doe Smith", "john.doe@keycloak.org"); + createUser("john.doe", "John", "Doe Smith", "john.doe@keycloak.org"); String search = "jo do"; assertThat(realm.admin().users().count(search), is(1)); @@ -86,24 +85,22 @@ public void searchUserMatchUsersCount() { */ @Test public void findUsersByEmailVerifiedStatus() { - UserRepresentation user1 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user1") .password("password") .name("user1FirstName", "user1LastName") .email("user1@example.com") .emailVerified() .enabled(true) - .build(); - realm.admin().users().create(user1); + .build()); - UserRepresentation user2 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user2") .password("password") .name("user2FirstName", "user2LastName") .email("user2@example.com") .enabled(true) - .build(); - realm.admin().users().create(user2); + .build()); boolean emailVerified; emailVerified = true; @@ -111,10 +108,95 @@ public void findUsersByEmailVerifiedStatus() { assertThat(usersEmailVerified, is(not(empty()))); assertThat(usersEmailVerified.get(0).getUsername(), is("user1")); + createUser(UserConfigBuilder.create() + .username("testuser2") + .password("password") + .name("testuser2", "testuser2") + .email("testuser2@example.com") + .emailVerified() + .enabled(true) + .build()); + + usersEmailVerified = realm.admin().users().search("user", null, null, null, emailVerified, null, null, null); + assertThat(usersEmailVerified, is(not(empty()))); + assertThat(usersEmailVerified.size(), is(1)); + assertThat(usersEmailVerified.get(0).getUsername(), is("user1")); + assertThat(realm.admin().users().count("user", null, null, null, emailVerified, null, null, null), is(1)); + emailVerified = false; List usersEmailNotVerified = realm.admin().users().search(null, null, null, null, emailVerified, null, null, null, true); assertThat(usersEmailNotVerified, is(not(empty()))); assertThat(usersEmailNotVerified.get(0).getUsername(), is("user2")); + + createUser(UserConfigBuilder.create() + .username("testuser3") + .password("password") + .name("testuser3", "testuser3") + .email("testuser3@example.com") + .enabled(true) + .build()); + + usersEmailVerified = realm.admin().users().search("user", null, null, null, emailVerified, null, null, null); + assertThat(usersEmailVerified, is(not(empty()))); + assertThat(usersEmailVerified.size(), is(1)); + assertThat(usersEmailVerified.get(0).getUsername(), is("user2")); + assertThat(realm.admin().users().count("user", null, null, null, emailVerified, null, null, null), is(1)); + } + + @Test + public void testCountUsersByEnabledStatus() { + createUser(UserConfigBuilder.create() + .username("user1") + .password("password") + .name("user1FirstName", "user1LastName") + .email("user1@example.com") + .emailVerified() + .enabled(true) + .build()); + + createUser(UserConfigBuilder.create() + .username("user2") + .password("password") + .name("user2FirstName", "user2LastName") + .email("user2@example.com") + .enabled(false) + .build()); + + assertThat(realm.admin().users().count("user", null, null, null, null, null, true, null), is(1)); + assertThat(realm.admin().users().count("user", null, null, null, null, null, false, null), is(1)); + } + + @Test + public void testCountUsersByFederatedIdentity() { + createUser(UserConfigBuilder.create() + .username("user1") + .password("password") + .name("user1FirstName", "user1LastName") + .email("user1@example.com") + .emailVerified() + .enabled(true) + .build()); + createUser(UserConfigBuilder.create() + .username("user2") + .password("password") + .name("user2FirstName", "user2LastName") + .email("user2@example.com") + .enabled(false) + .build()); + + runOnServer.run((session -> { + RealmModel realm = session.realms().getRealmByName(realmName); + session.getContext().setRealm(realm); + UserProvider users = session.users(); + users.addFederatedIdentity(realm, users.getUserById(realm, users.getUserByUsername(realm, "user1").getId()), new FederatedIdentityModel("user1Broker", "user1BrokerId", "user1BrokerUsername")); + users.addFederatedIdentity(realm, users.getUserById(realm, users.getUserByUsername(realm, "user2").getId()), new FederatedIdentityModel("user2Broker", "user2BrokerId", "user2BrokerUsername")); + })); + + assertThat(realm.admin().users().count(null, "user", null, null, null, null, null, "user1Broker", null, null), is(1)); + assertThat(realm.admin().users().count(null, "user", null, null, null, null, null, "user1Broker", "user1BrokerId", null), is(1)); + assertThat(realm.admin().users().count(null, "user", null, null, null, null, null, "user1Broker", "invalidId", null), is(0)); + assertThat(realm.admin().users().count(null, "user", null, null, null, null, false, "user2Broker", null, null), is(1)); + assertThat(realm.admin().users().count(null, "user", null, null, null, null, false, "user2Broker", "user1BrokerId", null), is(0)); } /** @@ -122,34 +204,31 @@ public void findUsersByEmailVerifiedStatus() { */ @Test public void countUsersByEmailVerifiedStatus() { - UserRepresentation user1 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user1") .password("password") .name("user1FirstName", "user1LastName") .email("user1@example.com") .emailVerified() .enabled(true) - .build(); - realm.admin().users().create(user1); + .build()); - UserRepresentation user2 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user2") .password("password") .name("user2FirstName", "user2LastName") .email("user2@example.com") .enabled(true) - .build(); - realm.admin().users().create(user2); + .build()); - UserRepresentation user3 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user3") .password("password") .name("user3FirstName", "user3LastName") .email("user3@example.com") .emailVerified() .enabled(true) - .build(); - realm.admin().users().create(user3); + .build()); boolean emailVerified; emailVerified = true; @@ -163,41 +242,38 @@ public void countUsersByEmailVerifiedStatus() { @Test public void countUsersWithViewPermission() { - createUser("user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); - createUser("user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + createUser("user1", "user1FirstName", "user1LastName", "user1@example.com"); + createUser("user2", "user2FirstName", "user2LastName", "user2@example.com"); assertThat(realm.admin().users().count(), is(2)); } @Test public void countUsersBySearchWithViewPermission() { - UserRepresentation user1 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user1") .password("password") .name("user1FirstName", "user1LastName") .email("user1@example.com") .emailVerified() .enabled(true) - .build(); - realm.admin().users().create(user1); + .build()); - UserRepresentation user2 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user2") .password("password") .name("user2FirstName", "user2LastName") .email("user2@example.com") .enabled(true) - .build(); - realm.admin().users().create(user2); + .build()); - UserRepresentation user3 = UserConfigBuilder.create() + createUser(UserConfigBuilder.create() .username("user3") .password("password") .name("user3FirstName", "user3LastName") .email("user3@example.com") .emailVerified() .enabled(true) - .build(); - realm.admin().users().create(user3); + .build()); // Prefix search count assertSearchMatchesCount(realm.admin(), "user", 3); @@ -231,8 +307,8 @@ public void countUsersBySearchWithViewPermission() { @Test public void countUsersByFiltersWithViewPermission() { - createUser("user1", "password", "user1FirstName", "user1LastName", "user1@example.com"); - createUser("user2", "password", "user2FirstName", "user2LastName", "user2@example.com"); + createUser("user1", "user1FirstName", "user1LastName", "user1@example.com"); + createUser("user2", "user2FirstName", "user2LastName", "user2@example.com"); //search username assertThat(realm.admin().users().count(null, null, null, "user"), is(2)); assertThat(realm.admin().users().count(null, null, null, "user1"), is(1)); @@ -284,4 +360,18 @@ private void assertCaseInsensitiveSearch() { assertThat(realm.admin().users().search("USER", true), hasSize(1)); assertThat(realm.admin().users().search("Use", true), hasSize(0)); } + + private void createUser(UserRepresentation user) { + realm.admin().users().create(user).close(); + } + + private void createUser(String username, String firstName, String lastName, String email) { + createUser(UserConfigBuilder.create() + .username(username) + .password("password") + .name(firstName, lastName) + .email(email) + .enabled(true) + .build()); + } } diff --git a/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java b/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java new file mode 100644 index 000000000000..0df0abffed04 --- /dev/null +++ b/tests/base/src/test/java/org/keycloak/tests/db/DbTest.java @@ -0,0 +1,29 @@ +package org.keycloak.tests.db; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.keycloak.config.DatabaseOptions; +import org.keycloak.quarkus.runtime.configuration.Configuration; +import org.keycloak.testframework.annotations.KeycloakIntegrationTest; +import org.keycloak.testframework.remote.runonserver.InjectRunOnServer; +import org.keycloak.testframework.remote.runonserver.RunOnServerClient; + +@KeycloakIntegrationTest +public class DbTest { + + @InjectRunOnServer + RunOnServerClient runOnServer; + + @Test + public void ensurePostgreSQLSettingsAreApplied() { + runOnServer.run(session -> { + if (Configuration.getConfigValue(DatabaseOptions.DB).getValue().equals("postgres") && + Configuration.getConfigValue(DatabaseOptions.DB_DRIVER).getValue().equals("org.postgresql.Driver")) { + Assertions.assertEquals("primary", Configuration.getConfigValue(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE).getValue()); + } else { + Assertions.assertNull(Configuration.getConfigValue(DatabaseOptions.DB_POSTGRESQL_TARGET_SERVER_TYPE).getValue()); + } + }); + } + +} diff --git a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java index 673fd5d4e74b..6c001f3c0380 100644 --- a/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java +++ b/tests/base/src/test/java/org/keycloak/tests/suites/DatabaseTestSuite.java @@ -4,6 +4,6 @@ import org.junit.platform.suite.api.Suite; @Suite -@SelectPackages({"org.keycloak.tests.admin"}) +@SelectPackages({"org.keycloak.tests.admin", "org.keycloak.tests.db"}) public class DatabaseTestSuite { } diff --git a/tests/clustering/README.md b/tests/clustering/README.md index ffcb2275b5a1..a2a96444ff5a 100644 --- a/tests/clustering/README.md +++ b/tests/clustering/README.md @@ -5,7 +5,10 @@ KC_TEST_SERVER_IMAGES -> if empty, uses the built distribution archive from quarkus/dist directory in all containers -> if single value, uses that value in all the containers -> if comma separated value ("imageA,imageB"), each container will use the image specified from the list. The number of items must match the cluster size. --> "-" special keyword to use the built distribution archive +-> "-" special keyword to use the built distribution archive. +> NOTE: If testing SNAPSHOT versions with "-", it's necessary for it to appear later in the CSV list than +> non-SNAPSHOT releases in order to avoid "Incorrect state of migration" exceptions. + KC_TEST_SERVER=cluster -> enables cluster mode (configured by default in clustering module) KC_TEST_DATABASE_INTERNAL=true -> configure keycloak with the internal database container IP instead of localhost (configured by default in clustering module) diff --git a/tests/clustering/src/test/resources/keycloak-test.properties b/tests/clustering/src/test/resources/keycloak-test.properties index 0b26865a61e0..db43aed9d3ba 100644 --- a/tests/clustering/src/test/resources/keycloak-test.properties +++ b/tests/clustering/src/test/resources/keycloak-test.properties @@ -9,6 +9,7 @@ kc.test.log.filter=true kc.test.log.category."org.keycloak.tests".level=INFO kc.test.log.category."testinfo".level=INFO +kc.test.log.category."org.keycloak.it".level=INFO kc.test.log.category."org.keycloak".level=WARN kc.test.log.category."managed.keycloak".level=WARN kc.test.log.category."managed.db".level=WARN diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java index ee13d407c00c..2b9111d4d192 100755 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/client/KeycloakTestingClient.java @@ -86,7 +86,7 @@ public TestingResource testing(String realm) { public void enableFeature(Profile.Feature feature) { String featureString; - if (Profile.getFeatureVersions(feature.getUnversionedKey()).size() > 1) { + if (shouldUseVersionedKey(feature)) { featureString = feature.getVersionedKey(); } else { featureString = feature.getKey(); @@ -96,9 +96,13 @@ public void enableFeature(Profile.Feature feature) { ProfileAssume.updateDisabledFeatures(disabledFeatures); } + private boolean shouldUseVersionedKey(Profile.Feature feature) { + return ((Profile.getFeatureVersions(feature.getUnversionedKey()).size() > 1) || (feature.getVersion() != 1)); + } + public void disableFeature(Profile.Feature feature) { String featureString; - if (Profile.getFeatureVersions(feature.getUnversionedKey()).size() > 1) { + if (shouldUseVersionedKey(feature)) { featureString = feature.getVersionedKey(); } else { featureString = feature.getKey(); @@ -115,7 +119,7 @@ public void disableFeature(Profile.Feature feature) { */ public void resetFeature(Profile.Feature feature) { String featureString; - if (Profile.getFeatureVersions(feature.getUnversionedKey()).size() > 1) { + if (shouldUseVersionedKey(feature)) { featureString = feature.getVersionedKey(); Profile.Feature featureVersionHighestPriority = Profile.getFeatureVersions(feature.getUnversionedKey()).iterator().next(); if (featureVersionHighestPriority.getType().equals(Profile.Feature.Type.DEFAULT)) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java index ca74e8777a19..460cfecac18d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/IllegalAdminUpgradeTest.java @@ -42,12 +42,15 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import org.keycloak.common.Profile; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import static org.keycloak.testsuite.auth.page.AuthRealm.TEST; /** * @author Bill Burke * @version $Revision: 1 $ */ +@EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ) public class IllegalAdminUpgradeTest extends AbstractKeycloakTest { public static final String CLIENT_NAME = "application"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index aad1e89800d6..d490a5503bc9 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -127,6 +127,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; +import java.util.function.Predicate; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; @@ -961,6 +962,33 @@ public void searchByAttributesWithPagination() { } } + @Test + public void searchByAttributesForAnyValue() { + createUser(UserBuilder.create().username("user-0").addAttribute("attr", "common").build()); + createUser(UserBuilder.create().username("user-1").addAttribute("test", "common").build()); + createUser(UserBuilder.create().username("user-2").addAttribute("test", "common").addAttribute("attr", "common").build()); + + Map attributes = new HashMap<>(); + attributes.put("attr", ""); + // exact needs to be set to false to match for any users with the attribute attr + List users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(2, users.size()); + assertTrue(users.stream().allMatch(r -> Set.of("user-0", "user-2").contains(r.getUsername()))); + + attributes = new HashMap<>(); + attributes.put("test", ""); + users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(2, users.size()); + assertTrue(users.stream().allMatch(r -> Set.of("user-1", "user-2").contains(r.getUsername()))); + + attributes = new HashMap<>(); + attributes.put("test", ""); + attributes.put("attr", ""); + users = realm.users().searchByAttributes(-1, -1, null, false, false, mapToSearchQuery(attributes)); + assertEquals(1, users.size()); + assertTrue(users.stream().allMatch(r -> "user-2".equals(r.getUsername()))); + } + @Test public void storeAndReadUserWithLongAttributeValue() { String longValue = RandomStringUtils.random(Integer.parseInt(DefaultAttributes.DEFAULT_MAX_LENGTH_ATTRIBUTES), true, true); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java index cea5513658d2..7b057ddede61 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerConfiguration.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Set; +import static org.keycloak.broker.oidc.OAuth2IdentityProviderConfig.TOKEN_ENDPOINT_URL; import static org.keycloak.testsuite.broker.BrokerTestConstants.*; import static org.keycloak.testsuite.broker.BrokerTestTools.*; @@ -204,7 +205,7 @@ protected void applyDefaultConfiguration(final Map config, Ident config.put("loginHint", "true"); config.put(OIDCIdentityProviderConfig.ISSUER, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME); config.put("authorizationUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/auth"); - config.put("tokenUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token"); + config.put(TOKEN_ENDPOINT_URL, getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/token"); config.put("logoutUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/logout"); config.put("userInfoUrl", getProviderRoot() + "/auth/realms/" + REALM_PROV_NAME + "/protocol/openid-connect/userinfo"); config.put("defaultScope", "email profile"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java index c034ca3c8e89..9fb1c4b075d9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcBrokerTokenExchangeTest.java @@ -80,7 +80,7 @@ import org.keycloak.util.BasicAuthHelper; /** - * Test for identity-provider token exchange scenarios. Base for tests of token-exchange V1 as well as token-exchange-federated V2 + * Test for identity-provider token exchange scenarios. Base for tests of token-exchange V1 */ @EnableFeatures({@EnableFeature(Profile.Feature.TOKEN_EXCHANGE), @EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)}) public class KcOidcBrokerTokenExchangeTest extends AbstractInitializedBaseBrokerTest { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ClientTokenExchangeSAML2Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ClientTokenExchangeSAML2Test.java index 2a2130102f6d..4c3468afac51 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ClientTokenExchangeSAML2Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ClientTokenExchangeSAML2Test.java @@ -57,6 +57,7 @@ import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.util.AdminClientUtil; @@ -90,6 +91,7 @@ */ @EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true) @EnableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, skipRestart = true) +@DisableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2, skipRestart = true) public class ClientTokenExchangeSAML2Test extends AbstractKeycloakTest { private static final String SAML_SIGNED_TARGET = "http://localhost:8080/saml-signed-assertion/"; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java new file mode 100644 index 000000000000..629fb8994a2c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/ExternalInternalTokenExchangeV2Test.java @@ -0,0 +1,327 @@ +/* + * 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.testsuite.oauth.tokenexchange; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.WebTarget; +import jakarta.ws.rs.core.Form; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; +import org.junit.Test; +import org.keycloak.OAuth2Constants; +import org.keycloak.admin.client.resource.ClientsResource; +import org.keycloak.admin.client.resource.IdentityProviderResource; +import org.keycloak.admin.client.resource.RealmResource; +import org.keycloak.authorization.model.Policy; +import org.keycloak.authorization.model.ResourceServer; +import org.keycloak.common.Profile; +import org.keycloak.models.ClientModel; +import org.keycloak.models.IdentityProviderModel; +import org.keycloak.models.IdentityProviderSyncMode; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.OIDCConfigAttributes; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.IdentityProviderRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; +import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation; +import org.keycloak.services.resources.admin.fgap.AdminPermissionManagement; +import org.keycloak.services.resources.admin.fgap.AdminPermissions; +import org.keycloak.testsuite.arquillian.annotation.EnableFeature; +import org.keycloak.testsuite.arquillian.annotation.EnableFeatures; +import org.keycloak.testsuite.broker.AbstractInitializedBaseBrokerTest; +import org.keycloak.testsuite.broker.BrokerConfiguration; +import org.keycloak.testsuite.broker.BrokerTestConstants; +import org.keycloak.testsuite.broker.KcOidcBrokerConfiguration; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.oauth.OAuthClient; +import org.keycloak.util.BasicAuthHelper; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.keycloak.broker.oidc.OAuth2IdentityProviderConfig.TOKEN_INTROSPECTION_URL; +import static org.keycloak.broker.oidc.OAuth2IdentityProviderConfig.TOKEN_ENDPOINT_URL; +import static org.keycloak.testsuite.broker.BrokerTestConstants.CLIENT_ID; +import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS; +import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME; +import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot; + +/** + * Test for external-internal token exchange using token_exchange_external_internal:v2 + * + * @author Marek Posolda + */ +// TODO: Remove fine grained admin permissions should not be needed. They are neded now for token_exchange_external_internal:v2, but should not be needed in the future +@EnableFeatures({@EnableFeature(Profile.Feature.TOKEN_EXCHANGE_EXTERNAL_INTERNAL_V2), @EnableFeature(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ)}) +public class ExternalInternalTokenExchangeV2Test extends AbstractInitializedBaseBrokerTest { + + @Override + protected BrokerConfiguration getBrokerConfiguration() { + return new KcOidcBrokerConfiguration() { + + @Override + protected void applyDefaultConfiguration(Map config, IdentityProviderSyncMode syncMode) { + super.applyDefaultConfiguration(config, syncMode); + config.put(TOKEN_INTROSPECTION_URL, config.get(TOKEN_ENDPOINT_URL) + "/introspect"); + } + + @Override + public List createProviderClients() { + List providerClients = super.createProviderClients(); + ClientRepresentation brokerApp = providerClients.stream() + .filter(client -> CLIENT_ID.equals(client.getClientId())) + .findFirst().get(); + brokerApp.setDirectAccessGrantsEnabled(true); + + ClientRepresentation client2 = createProviderClientWithAudienceMapper("client-with-brokerapp-audience", CLIENT_ID); + ClientRepresentation client3 = createProviderClientWithAudienceMapper("client-with-consumer-realm-issuer-audience", getProviderRoot() + "/auth/realms/" + REALM_CONS_NAME); + ClientRepresentation client4 = createProviderClientWithAudienceMapper("client-without-valid-audience", "some-random-audience"); + + providerClients = new ArrayList<>(providerClients); + providerClients.addAll(Arrays.asList(client2, client3, client4)); + return providerClients; + } + + private ClientRepresentation createProviderClientWithAudienceMapper(String clientId, String hardcodedAudience) { + ClientRepresentation client = new ClientRepresentation(); + client.setClientId(clientId); + client.setSecret("secret"); + client.setDirectAccessGrantsEnabled(true); + + ProtocolMapperRepresentation hardcodedAudienceMapper = new ProtocolMapperRepresentation(); + hardcodedAudienceMapper.setName("audience"); + hardcodedAudienceMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + hardcodedAudienceMapper.setProtocolMapper(AudienceProtocolMapper.PROVIDER_ID); + + Map hardcodedAudienceMapperConfig = hardcodedAudienceMapper.getConfig(); + hardcodedAudienceMapperConfig.put(AudienceProtocolMapper.INCLUDED_CUSTOM_AUDIENCE, hardcodedAudience); + hardcodedAudienceMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true"); + + client.setProtocolMappers(Collections.singletonList(hardcodedAudienceMapper)); + return client; + } + + }; + } + + private static void setupRealm(KeycloakSession session) { + RealmModel realm = session.getContext().getRealm(); + IdentityProviderModel idp = session.identityProviders().getByAlias(IDP_OIDC_ALIAS); + org.junit.Assert.assertNotNull(idp); + + ClientModel client = realm.addClient("test-app"); + client.setClientId("test-app"); + client.setPublicClient(false); + client.setDirectAccessGrantsEnabled(true); + client.setEnabled(true); + client.setSecret("secret"); + client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + client.setFullScopeAllowed(false); + client.setRedirectUris(Set.of(OAuthClient.AUTH_SERVER_ROOT + "/*")); + + ClientModel brokerApp = realm.getClientByClientId("broker-app"); + + AdminPermissionManagement management = AdminPermissions.management(session, realm); + management.idps().setPermissionsEnabled(idp, true); + ClientPolicyRepresentation clientRep = new ClientPolicyRepresentation(); + clientRep.setName("toIdp"); + clientRep.addClient(client.getId(), brokerApp.getId()); + ResourceServer server = management.realmResourceServer(); + Policy clientPolicy = management.authz().getStoreFactory().getPolicyStore().create(server, clientRep); + management.idps().exchangeToPermission(idp).addAssociatedPolicy(clientPolicy); + + realm = session.realms().getRealmByName(BrokerTestConstants.REALM_PROV_NAME); + client = realm.getClientByClientId("brokerapp"); + client.addRedirectUri(OAuthClient.APP_ROOT + "/auth"); + client.setAttribute(OIDCConfigAttributes.BACKCHANNEL_LOGOUT_URL, OAuthClient.APP_ROOT + "/admin/backchannelLogout"); + } + + + @Test + public void testSuccess_externalTokenIssuedToBrokerClient() throws Exception { + ClientRepresentation brokerApp = getBrokerAppClient(); + + // Send initial direct-grant request + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client(brokerApp.getClientId(), brokerApp.getSecret()).doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + // Send token-exchange + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(200)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), notNullValue()); + }); + } + + + @Test + public void testSuccess_brokerClientAsAudienceOfExternalToken() throws Exception { + // Send initial direct-grant request. Token is issued to the "client-with-brokerapp-audience". Client "brokerapp" is available inside token audience + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client("client-with-brokerapp-audience", "secret").doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(200)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), notNullValue()); + }); + } + + + @Test + public void testSuccess_consumerRealmIssuerAsAudienceOfExternalToken() throws Exception { + // Send initial direct-grant request. Token is issued to the "client-with-consumer-realm-issuer-audience". Consumer realm is available inside token audience and hence token considered as valid external token for the token exchange + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client("client-with-consumer-realm-issuer-audience", "secret").doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(200)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), notNullValue()); + }); + } + + + @Test + public void testFailure_externalTokenIssuedToInvalidClient() throws Exception { + // Send initial direct-grant request. Token is issued to the "client-without-valid-audience". This external token will fail token-exchange as token is not issued to brokerapp and there is not any valid audience available + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client("client-without-valid-audience", "secret").doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(400)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), nullValue()); + assertEquals("Token not authorized for token exchange", externalToInternalTokenResponse.getErrorDescription()); + }); + } + + @Test + public void testFailure_externalTokenIntrospectionFailureDueInvalidClientCredentials() throws Exception { + // Update IDP and set invalid credentials there + IdentityProviderResource idpResource = adminClient.realm(REALM_CONS_NAME).identityProviders().get(IDP_OIDC_ALIAS); + IdentityProviderRepresentation idpRep = idpResource.toRepresentation(); + idpRep.getConfig().put("clientSecret", "invalid"); + idpResource.update(idpRep); + + ClientRepresentation brokerApp = getBrokerAppClient(); + + try { + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client(brokerApp.getClientId(), brokerApp.getSecret()).doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(400)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), nullValue()); + assertEquals("Introspection endpoint call failure. Introspection response status: 401", externalToInternalTokenResponse.getErrorDescription()); + }); + } finally { + // Revert IDP config + idpRep.getConfig().put("clientSecret", brokerApp.getSecret()); + idpResource.update(idpRep); + } + } + + + @Test + public void testFailure_inactiveExternalToken() throws Exception { + ClientRepresentation brokerApp = getBrokerAppClient(); + org.keycloak.testsuite.util.oauth.AccessTokenResponse tokenResponse = oauth.realm(bc.providerRealmName()).client(brokerApp.getClientId(), brokerApp.getSecret()).doPasswordGrantRequest(bc.getUserLogin(), bc.getUserPassword()); + assertThat(tokenResponse.getIdToken(), notNullValue()); + + testingClient.server(BrokerTestConstants.REALM_CONS_NAME).run(ExternalInternalTokenExchangeV2Test::setupRealm); + + setTimeOffset(3600); + + testTokenExchange(tokenResponse.getAccessToken(), (tokenExchangeResponse) -> { + assertThat(tokenExchangeResponse.getStatus(), equalTo(400)); + AccessTokenResponse externalToInternalTokenResponse = tokenExchangeResponse.readEntity(AccessTokenResponse.class); + assertThat(externalToInternalTokenResponse.getToken(), nullValue()); + assertEquals("Token not active", externalToInternalTokenResponse.getErrorDescription()); + }); + } + + private void testTokenExchange(String subjectToken, Consumer tokenExchangeResponseConsumer) { + // Send token-exchange + try (Client httpClient = AdminClientUtil.createResteasyClient()) { + WebTarget exchangeUrl = getConsumerTokenEndpoint(httpClient); + + String subjectTokenType = OAuth2Constants.ACCESS_TOKEN_TYPE; // hardcoded to access-token just for now. More types might need to be tested... + try (Response response = sendExternalInternalTokenExchangeRequest(exchangeUrl, subjectToken, subjectTokenType)) { + tokenExchangeResponseConsumer.accept(response); + } + } + } + + private Response sendExternalInternalTokenExchangeRequest(WebTarget exchangeUrl, String subjectToken, String subjectTokenType) { + return exchangeUrl.request() + .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader( + "test-app", "secret")) + .post(Entity.form( + new Form() + .param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.TOKEN_EXCHANGE_GRANT_TYPE) + .param(OAuth2Constants.SUBJECT_TOKEN, subjectToken) + .param(OAuth2Constants.SUBJECT_TOKEN_TYPE, subjectTokenType) + .param(OAuth2Constants.SUBJECT_ISSUER, bc.getIDPAlias()) + .param(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID) + + )); + } + + private WebTarget getConsumerTokenEndpoint(Client httpClient) { + return httpClient.target(OAuthClient.AUTH_SERVER_ROOT) + .path("/realms") + .path(bc.consumerRealmName()) + .path("protocol/openid-connect/token"); + } + + private ClientRepresentation getBrokerAppClient() { + RealmResource providerRealm = realmsResouce().realm(bc.providerRealmName()); + ClientsResource clients = providerRealm.clients(); + return clients.findByClientId("brokerapp").get(0); + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV1Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV1Test.java index 82c7f87b145a..a286d12847c8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV1Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV1Test.java @@ -35,6 +35,7 @@ import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; @@ -43,7 +44,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -58,6 +58,7 @@ */ @EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true) @EnableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, skipRestart = true) +@DisableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2, skipRestart = true) public class StandardTokenExchangeV1Test extends AbstractKeycloakTest { @Rule diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV2WithLegacyTokenExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV2WithLegacyTokenExchangeTest.java index a8615e3681b4..60f283cdcde9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV2WithLegacyTokenExchangeTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/StandardTokenExchangeV2WithLegacyTokenExchangeTest.java @@ -23,6 +23,7 @@ import org.junit.Test; import org.keycloak.OAuthErrorException; import org.keycloak.common.Profile; +import org.keycloak.testsuite.arquillian.annotation.DisableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected; import org.keycloak.testsuite.util.oauth.AccessTokenResponse; @@ -36,6 +37,7 @@ */ @EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true) @EnableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, skipRestart = true) +@DisableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2, skipRestart = true) public class StandardTokenExchangeV2WithLegacyTokenExchangeTest extends StandardTokenExchangeV2Test { @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java index 760da1020252..cd32a645ad62 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/tokenexchange/SubjectImpersonationTokenExchangeV1Test.java @@ -63,6 +63,7 @@ */ @EnableFeature(value = Profile.Feature.TOKEN_EXCHANGE, skipRestart = true) @EnableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, skipRestart = true) +@DisableFeature(value = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2, skipRestart = true) public class SubjectImpersonationTokenExchangeV1Test extends AbstractKeycloakTest { @Rule diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java index b8c048f3638a..032018aea497 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCIssuerEndpointTest.java @@ -408,8 +408,14 @@ protected Map getCredentialDefinitionAttributes() { protected static class CredentialResponseHandler { protected void handleCredentialResponse(CredentialResponse credentialResponse) throws VerificationException { - assertNotNull("The credential should have been responded.", credentialResponse.getCredential()); - JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken(); + assertNotNull("The credentials array should be present in the response.", credentialResponse.getCredentials()); + assertFalse("The credentials array should not be empty.", credentialResponse.getCredentials().isEmpty()); + + // Get the first credential from the array (maintaining compatibility with single credential tests) + CredentialResponse.Credential credentialObj = credentialResponse.getCredentials().get(0); + assertNotNull("The first credential in the array should not be null.", credentialObj); + + JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialObj.getCredential(), JsonWebToken.class).getToken(); assertEquals("did:web:test.org", jsonWebToken.getIssuer()); VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class); assertEquals(List.of("VerifiableCredential"), credential.getType()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java index 8400cf42ef0b..efb2c910a96e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCJWTIssuerEndpointTest.java @@ -333,7 +333,7 @@ public void testRequestCredential() { assertEquals("The credential request should be answered successfully.", HttpStatus.SC_OK, credentialResponse.getStatus()); assertNotNull("A credential should be responded.", credentialResponse.getEntity()); CredentialResponse credentialResponseVO = JsonSerialization.mapper.convertValue(credentialResponse.getEntity(), CredentialResponse.class); - JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredential(), JsonWebToken.class).getToken(); + JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponseVO.getCredentials().get(0).getCredential(), JsonWebToken.class).getToken(); assertNotNull("A valid credential string should have been responded", jsonWebToken); assertNotNull("The credentials should be included at the vc-claim.", jsonWebToken.getOtherClaims().get("vc")); @@ -442,7 +442,7 @@ public void testCredentialIssuanceWithAuthZCodeWithScopeMatched() throws Excepti assertEquals(200, response.getStatus()); CredentialResponse credentialResponse = JsonSerialization.readValue(response.readEntity(String.class), CredentialResponse.class); - JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredential(), JsonWebToken.class).getToken(); + JsonWebToken jsonWebToken = TokenVerifier.create((String) credentialResponse.getCredentials().get(0).getCredential(), JsonWebToken.class).getToken(); assertEquals("did:web:test.org", jsonWebToken.getIssuer()); VerifiableCredential credential = JsonSerialization.mapper.convertValue(jsonWebToken.getOtherClaims().get("vc"), VerifiableCredential.class); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java index 2911cc05e0fc..9b13e1c68fe2 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oid4vc/issuance/signing/OID4VCSdJwtIssuingEndpointTest.java @@ -233,7 +233,8 @@ private static SdJwtVP testRequestTestCredential(KeycloakSession session, String CredentialResponse credentialResponseVO = JsonSerialization.mapper.convertValue(credentialResponse.getEntity(), CredentialResponse.class); new TestCredentialResponseHandler(vct).handleCredentialResponse(credentialResponseVO); - return SdJwtVP.of(credentialResponseVO.getCredential().toString()); + // Get the credential from the credentials array + return SdJwtVP.of(credentialResponseVO.getCredentials().get(0).getCredential().toString()); } // Tests the complete flow from @@ -445,7 +446,7 @@ static class TestCredentialResponseHandler extends CredentialResponseHandler { @Override protected void handleCredentialResponse(CredentialResponse credentialResponse) throws VerificationException { // SDJWT have a special format. - SdJwtVP sdJwtVP = SdJwtVP.of(credentialResponse.getCredential().toString()); + SdJwtVP sdJwtVP = SdJwtVP.of(credentialResponse.getCredentials().get(0).getCredential().toString()); JsonWebToken jsonWebToken = TokenVerifier.create(sdJwtVP.getIssuerSignedJWT().toJws(), JsonWebToken.class).getToken(); assertNotNull("A valid credential string should have been responded", jsonWebToken); diff --git a/themes/src/main/resources-community/theme/base/account/messages/messages_nl.properties b/themes/src/main/resources-community/theme/base/account/messages/messages_nl.properties index 05dc215e87f7..774b1ce52c1d 100644 --- a/themes/src/main/resources-community/theme/base/account/messages/messages_nl.properties +++ b/themes/src/main/resources-community/theme/base/account/messages/messages_nl.properties @@ -124,9 +124,9 @@ offlineToken=Offline token revoke=Vergunning intrekken configureAuthenticators=Ingestelde authenticators mobile=Mobiel -totpStep1=Installeer een van de volgende applicaties op je mobiele apparaat: -totpStep2=Open de applicatie en scan de barcode: -totpStep3=Voer de eenmalige code in – gegenereerd door de applicatie – en tik op ''Opslaan'' om de installatie te voltooien. +totpStep1=Installeer een van de volgende apps op je mobiel: +totpStep2=Open de app en scan de barcode: +totpStep3=Voer de eenmalige code in – gegenereerd door de app – en tik op ''Opslaan'' om de installatie te voltooien. totpStep3DeviceName=Geef je apparaat een naam om je OTP-apparaten overzichtelijk te houden. totpManualStep2=Open de applicatie en voer de sleutel in: totpManualStep3=Gebruik de volgende configuratiewaarden als de applicatie deze instellingen toestaat: @@ -146,7 +146,7 @@ irreversibleAction=Deze actie is onomkeerbaar deletingImplies=Het verwijderen van je account houdt in: errasingData=Al uw gegevens wissen loggingOutImmediately=Meteen uitloggen -accountUnusable=Dit account kan niet meer gebruikt worden voor gebruik in de applicatie +accountUnusable=Dit account kan niet meer gebruikt worden in de applicatie missingUsernameMessage=Gebruikersnaam ontbreekt. missingFirstNameMessage=Voornaam ontbreekt. invalidEmailMessage=Ongeldig e-mailadres. @@ -164,8 +164,8 @@ invalidTotpMessage=Ongeldige authenticatiecode. usernameExistsMessage=Gebruikersnaam al in gebruik. emailExistsMessage=E-mailadres al in gebruik. readOnlyUserMessage=Je kunt je account niet bijwerken omdat deze read-only is. -readOnlyUsernameMessage=U kunt uw gebruikersnaam niet wijzigen omdat uw account alleen-lezen is. -readOnlyPasswordMessage=U kunt uw wachtwoord niet wijzigen omdat uw account alleen-lezen is. +readOnlyUsernameMessage=U kunt uw gebruikersnaam niet wijzigen omdat uw account read-only is. +readOnlyPasswordMessage=U kunt uw wachtwoord niet wijzigen omdat uw account read-only is. successTotpMessage=Mobiele authenticatie geconfigureerd. successTotpRemovedMessage=Mobiele authenticator verwijderd. successGrantRevokedMessage=Toestemming succesvol ingetrokken. @@ -220,7 +220,7 @@ owner=Eigenaar resourcesSharedWithMe=Bronnen die met mij gedeeld zijn permissionRequestion=Toestemmingsverzoek permission=Toestemming -shares=aandelen +shares=Gedeeld met notBeingShared=Deze bron wordt niet gedeeld. notHaveAnyResource=U heeft geen bronnen noResourcesSharedWithYou=Er zijn geen bronnen met u gedeeld @@ -345,7 +345,7 @@ federatedIdentityBoundOrganization=Koppeling met een organisatiegebonden identit accountManagementBaseThemeCannotBeUsedDirectly=Het basis account-thema heeft alleen vertalingen voor de accountconsole. Voor het weergeven van de accountconsole moet je of het hoofdthema van je thema instellen op een ander account-thema, of je eigen index.ftl bestand toevoegen. Bekijk de documentatie voor meer info. currentPassword=Huidig wachtwoord personalInfoHtmlTitle=Persoonlijke info -deviceActivityHtmlTitle=Activiteit apparaat +deviceActivityHtmlTitle=Apparaat activiteit linkedAccountsHtmlTitle=Gekoppelde accounts device-activity=Apparaat activiteit availableRoles=Beschikbare rollen diff --git a/themes/src/main/resources-community/theme/base/admin/messages/messages_ja.properties b/themes/src/main/resources-community/theme/base/admin/messages/messages_ja.properties index 3543d992628b..226e3399273f 100644 --- a/themes/src/main/resources-community/theme/base/admin/messages/messages_ja.properties +++ b/themes/src/main/resources-community/theme/base/admin/messages/messages_ja.properties @@ -27,8 +27,8 @@ pairwiseRedirectURIsMismatch=クライアントのリダイレクトURIが、セ invalidPasswordMaxLengthMessage=無効なパスワード: 最大長は{0}です。 invalidPasswordNotEmailMessage=無効なパスワード: メールアドレスと同じパスワードは禁止されています。 ldapErrorEditModeMandatory=編集モードは必須です -ldapErrorValidatePasswordPolicyAvailableForWritableOnly=パスワードポリシーの検証はWRITABLE編集モードでのみ適用されます。 -clientRedirectURIsInvalid=リダイレクトURLは有効なURLではありません。 +ldapErrorValidatePasswordPolicyAvailableForWritableOnly=パスワードポリシーの検証はWRITABLE編集モードでのみ適用されます +clientRedirectURIsInvalid=リダイレクトURLは有効なURLではありません backchannelLogoutUrlIsInvalid=バックチャネルログアウトURLは有効なURLではありません duplicatedJwksSettings=「JWKSを使用する」スイッチと「JWKS URLを使用する」スイッチを同時にオンにすることはできません。 error-invalid-value=無効な値です。 diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_ka.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_ka.properties index 6b22def79d7e..851139626304 100644 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_ka.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_ka.properties @@ -6,9 +6,8 @@ orgInviteBodyHtml=

მოგიწვიეს, მიუერთდეთ orgInviteBodyPersonalized=გამარჯობა, "{5}" "{6}"\n\n თქვენ მიგიწვიეს, შეუერთდეთ ორგანიზაციას "{3}". ამისთვის დააწკაპუნეთ ქვედა ბმულს.\n\n{0}\n\nბმულის ვადის ამოწურვის დროა {4}.\n\nთუ არ გსურთ, ორგანიზაციას მიუერთდეთ, უბრალოდ გამოტოვეთ ეს შეტყობინება. emailUpdateConfirmationBody=იმისათვის, რომ განაახლოთ თქვენი {2}-ის ანგარიში ელფოსტის ანგარიშით {1}, დააწკაპუნეთ ქვედა ბმულზე\n\n{0}\n\nამ ბმული ვადის ამოწურვის დროა {3}.\n\nთუ არ გნებავთ ამ ცვლილების შეტანა, უბრალოდ გამოტოვეთ ეს შეტყობინება. orgInviteBodyPersonalizedHtml=

{5} {6}

გამარჯობა. თქვენ მიგიწვიეს, შეუერთდეთ ორგანიზაციას "{3}". ამისთვის დააწკაპუნეთ ქვედა ბმულს.

ბმული ორგანიზაციასთან მისაერთებლად

ბმულის ვადის ამოწურვის დროა {4}.

თუ არ გსურთ, ორგანიზაციას მიუერთდეთ, უბრალოდ გამოტოვეთ ეს შეტყობინება.

-identityProviderLinkBody=ვიღაცას უნდა, თქვენი "{1}" ანგარიში მიაბას "{0}" ანგარიშს მომხმარებლისთვის {2} . თუ ეს თქვენ ბრძანდებით, დააწკაპუნეთ ქვედა ბმულზე, ანგარიშის მისაბმელად\n\n{3}\n\nამ ბმულის ვადის ამოწურვის დროა {5}.\n\nთუ არ გნებავთ ანგარიშის მიბმა, უბრალოდ გამოტოვეთ ეს შეტყობინება. თუ ანგარიშებს მიაბამთ, შეგეძლებათ შეხვიდეთ {1}-დან {0}-მდე ყველაფერზე. +identityProviderLinkBody=ვიღაცას უნდა, თქვენი "{1}" ანგარიში მიაბას "{0}" ანგარიშს მომხმარებლისთვის {2} . თუ ეს თქვენ ბრძანდებით, დააწკაპუნეთ ქვედა ბმულზე, ანგარიშის მისაბმელად\n\n{3}\n\nამ ბმულის ვადის ამოწურვის დროა {5}.\n\nთუ თქვენ არ დაგიწყიათ ეს პროცესი, ან არ გნებავთ ანგარიშის მიბმა, უბრალოდ გამოტოვეთ ეს შეტყობინება. თუ ანგარიშებს მიაბამთ, შეგეძლებათ შეხვიდეთ {1}-დან {0}-მდე ყველაფერზე. executeActionsBody=თქვენმა ადმინისტრატორმა მოითხოვა, რომ თქვენ განაახლოთ {2} ანგარიში, შემდეგი ქმედებების შესრულებით: {3}. დააწკაპუნეთ ქვედა ბმულზე, რომ ეს პროცესი დაიწყოთ\n\n{0}\n\nამ ბმულის ვადის ამოწურვის დროა {4}.\n\nთუ თქვენ არ გგონიათ, რომ ეს თქვენმა სისტემურმა ადმინისტრატორმა მოგთხოვთ, უბრალოდ გამოტოვეთ ეს შეტყობინება და არაფერი შეიცვლება. -identityProviderLinkBodyHtml=

ვიღაცას უნდა, თქვენი {1} ანგარიში მიაბას {0} ანგარიშს მომხმარებლისთვის {2} . თუ ეს თქვენ ბრძანდებით, დააწკაპუნეთ ქვედა ბმულზე, ანგარიშის მისაბმელად

ბმული ანგარიშის მიბმის დასადასტურებლად.

ამ ბმულის ვადის ამოწურვის დროა {5}.

თუ არ გნებავთ ანგარიშის მიბმა, უბრალოდ გამოტოვეთ ეს შეტყობინება. თუ ანგარიშებს მიაბამთ, შეგეძლებათ შეხვიდეთ {1}-დან {0}-მდე ყველაფერზე.

passwordResetBody=ვიღაცამ თქვენი {2} ანგარიშის ავტორიზაციის დეტალების შეცვლა მოინდომა. თუ ეს თქვენ ბრძანდებოდით, მათ ჩამოსაყრელად ქვედა ბმულს დააჭირეთ.\n\n{0}\n\nეს ბმულის და კოდის ვადის ამოწურვის დროა {3}.\n\nთუ არ გნებავთ თქვენი ავტორიზაციის დეტალების ჩამოყრა, უბრალოდ გამოტოვეთ ეს შეტყობინება და არაფერი შეიცვლება. passwordResetBodyHtml=

ვიღაცამ თქვენი {2} ანგარიშის ავტორიზაციის დეტალების შეცვლა მოინდომა. თუ ეს თქვენ ბრძანდებოდით, მათ ჩამოსაყრელად ქვედა ბმულს დააჭირეთ.

ბმული ავტორიზაციის დეტალების ჩამოსაყრელად.

ეს ბმულის და კოდის ვადის ამოწურვის დროა {3}.

თუ არ გნებავთ თქვენი ავტორიზაციის დეტალების ჩამოყრა, უბრალოდ გამოტოვეთ ეს შეტყობინება და არაფერი შეიცვლება.

executeActionsBodyHtml=

თქვენმა ადმინისტრატორმა მოითხოვა, რომ თქვენ განაახლოთ {2} ანგარიში, შემდეგი ქმედებების შესრულებით: {3}. დააწკაპუნეთ ქვედა ბმულზე, რომ ეს პროცესი დაიწყოთ.

ბმული ანგარიშის განახლებაზე

ამ ბმულის ვადის ამოწურვის დროა {4}.

თუ თქვენ არ გგონიათ, რომ ეს თქვენმა სისტემურმა ადმინისტრატორმა მოგთხოვთ, უბრალოდ გამოტოვეთ ეს შეტყობინება და არაფერი შეიცვლება.

diff --git a/themes/src/main/resources-community/theme/base/email/messages/messages_pt_BR.properties b/themes/src/main/resources-community/theme/base/email/messages/messages_pt_BR.properties index 1befd62c003a..749a5475d8a0 100644 --- a/themes/src/main/resources-community/theme/base/email/messages/messages_pt_BR.properties +++ b/themes/src/main/resources-community/theme/base/email/messages/messages_pt_BR.properties @@ -5,8 +5,8 @@ emailTestSubject=[KEYCLOAK] - Mensagem de teste SMTP emailTestBody=Esta é uma mensagem de teste emailTestBodyHtml=

Esta é uma mensagem de teste

identityProviderLinkSubject=Vincular {0} -identityProviderLinkBody=Alguém quer vincular a sua conta "{1}" com a conta "{0}" do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.\n\n{3}\n\nEste link irá expirar em {5}.\n\nSe você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} fazendo login em {0}. -identityProviderLinkBodyHtml=

Alguém quer vincular a sua conta {1} com a conta {0} do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.

Link para confirmar vinculação de contas

Este link irá expirar em {5}.

Se você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} fazendo login em {0}.

+identityProviderLinkBody=Alguém quer vincular a sua conta "{1}" com a conta "{0}" do usuário {2} . Se foi você, clique no link abaixo para vincular as contas\n\n{3}\n\nEste link expirará em {5}.\n\nSe você não iniciou este processo ou não deseja vincular as contas, apenas ignore esta mensagem. Se você vincular as contas, poderá fazer login em {1} através de {0}. +identityProviderLinkBodyHtml=

Alguém quer vincular a sua conta {1} com a conta {0} do usuário {2}. Se foi você, clique no link abaixo para vincular as contas

Link para confirmar vinculação de contas

Este link expirarpa em {5}.

Se você não iniciou este processo ou não deseja vincular as contas, apenas ignore esta mensagem. Se você vincular as contas, poderá fazer login em {1} através de {0}.

passwordResetSubject=Redefinir senha passwordResetBody=Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.\n\n{0}\n\nEste link e código expiram em {3}.\n\nSe você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado. passwordResetBodyHtml=

Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.

Link para redefinir a senha

Este link irá expirar em {3}.

Se você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado.