Compare commits

..

95 commits

Author SHA1 Message Date
9bf20bb2cd Provide SCIM2 client capabilities behing an experimental Feature Profile
Some checks are pending
Keycloak CI / Base IT (new) (push) Blocked by required conditions
Keycloak CI / Status Check - Keycloak CI (push) Blocked by required conditions
CodeQL / Check conditional workflows and jobs (push) Waiting to run
CodeQL / CodeQL Java (push) Blocked by required conditions
CodeQL / CodeQL JavaScript (push) Blocked by required conditions
CodeQL / CodeQL TypeScript (push) Blocked by required conditions
CodeQL / Status Check - CodeQL (push) Blocked by required conditions
Keycloak Guides / Check conditional workflows and jobs (push) Waiting to run
Keycloak Guides / Build (push) Blocked by required conditions
Keycloak Guides / Status Check - Keycloak Guides (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI E2E (push) Blocked by required conditions
Keycloak JavaScript CI / Status Check - Keycloak JavaScript CI (push) Blocked by required conditions
Keycloak JavaScript CI / Build Keycloak (push) Blocked by required conditions
Keycloak JavaScript CI / Admin Client (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI E2E (push) Blocked by required conditions
Keycloak Documentation / Check conditional workflows and jobs (push) Waiting to run
Keycloak Documentation / Build (push) Blocked by required conditions
Keycloak Documentation / External links check (push) Blocked by required conditions
Keycloak Documentation / Status Check - Keycloak Documentation (push) Blocked by required conditions
Keycloak JavaScript CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak JavaScript CI / UI Shared (push) Blocked by required conditions
Keycloak JavaScript CI / Generate Test Seed (push) Blocked by required conditions
Keycloak Operator CI / Test remote (push) Blocked by required conditions
Keycloak Operator CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak Operator CI / Build distribution (push) Blocked by required conditions
Keycloak Operator CI / Test local (push) Blocked by required conditions
Keycloak Operator CI / Test OLM installation (push) Blocked by required conditions
Keycloak Operator CI / Status Check - Keycloak Operator CI (push) Blocked by required conditions
Closes #1234

Signed-off-by: Alex Morel <amorel@codelutin.com>
2024-11-07 11:05:29 +01:00
Lukas Hanusovsky
5ba1efc858
Surefire reports - support 26.0 release branch
Closes #34681

Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
2024-11-07 10:59:40 +01:00
vramik
a2ba3c8ace Feature in higher version takes precedence even if it has lower type order
Closes #34635

Signed-off-by: vramik <vramik@redhat.com>
2024-11-07 10:55:42 +01:00
vramik
b1ff9511d1 Fine grained admin permissions feature V2
Closes #34563

Signed-off-by: vramik <vramik@redhat.com>
2024-11-07 10:55:42 +01:00
Pedro Ruivo
33cae33ae4
Remove JGroups thread pool docs from HA Guide
Clustering is disabled with multi-site deployment and there is no
JGroups thread pool to configure.

Closes #34715

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
2024-11-07 09:00:48 +00:00
Ricardo Martin
226daa41c7
Add service account mappers via client scope instead of dedicated scope (#34664)
Closes #10417

Signed-off-by: rmartinc <rmartinc@redhat.com>


Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
Signed-off-by: Ricardo Martin <rmartinc@redhat.com>
2024-11-07 08:45:11 +01:00
Thomas Darimont
fec661cf10 Allow OIDCIdentityProvider implementations to override isTokenTypeSupported
Fixes #34695

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
2024-11-06 16:28:44 +01:00
rmartinc
d2e19da64e Avoid using nip.io resolution in RefreshTokenTest#refreshTokenWithDifferentIssuer test
Closes #25675

Signed-off-by: rmartinc <rmartinc@redhat.com>
2024-11-06 14:53:19 +01:00
Jon Koops
b2930a4799
Use a hidden form to do POST based logout (#34694)
Closes #32648

Signed-off-by: Jon Koops <jonkoops@gmail.com>
2024-11-06 14:02:30 +01:00
dependabot[bot]
a9c3e592f3
Bump cypress from 13.15.1 to 13.15.2 (#34679)
Bumps [cypress](https://github.com/cypress-io/cypress) from 13.15.1 to 13.15.2.
- [Release notes](https://github.com/cypress-io/cypress/releases)
- [Changelog](https://github.com/cypress-io/cypress/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/cypress-io/cypress/compare/v13.15.1...v13.15.2)

---
updated-dependencies:
- dependency-name: cypress
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 09:13:32 +01:00
Lukas Hanusovsky
a8d9a5553f
[Test framework] Add custom provider dependencies into a Keycloak server (#34621)
* Add custom provider dependencies into a Keycloak server.

Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
Co-authored-by: Simon Vacek <svacek@redhat.com>

* Update test-framework/examples/pom.xml

Signed-off-by: Stian Thorgersen <stian@redhat.com>

---------

Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
Signed-off-by: Stian Thorgersen <stian@redhat.com>
Co-authored-by: Simon Vacek <svacek@redhat.com>
Co-authored-by: Stian Thorgersen <stian@redhat.com>
2024-11-06 08:39:28 +01:00
Ricardo Martin
ce454bda47
Remove online session when offline access is requested as the first request (#34346)
Closes #34001

Signed-off-by: rmartinc <rmartinc@redhat.com>


Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
Signed-off-by: Marek Posolda <mposolda@gmail.com>

---------

Signed-off-by: rmartinc <rmartinc@redhat.com>
Signed-off-by: Marek Posolda <mposolda@gmail.com>
Co-authored-by: Marek Posolda <mposolda@gmail.com>
Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
2024-11-06 08:33:12 +01:00
Jon Koops
b44aee7535
Use a weekly cache key for PNPM store (#34656)
Closes #34655

Signed-off-by: Jon Koops <jonkoops@gmail.com>
2024-11-06 08:25:49 +01:00
Lasse Bak Pedersen
65e90d2ff4
Added missing Danish translation keys in messages_da.properties (#34588)
Signed-off-by: Lasse Pedersen <laped87@gmail.com>
2024-11-05 18:50:07 +00:00
Pedro Ruivo
36defd5f33 cache-embedded-mtls-enabled is ignored
Fixes #34644

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
2024-11-05 18:57:22 +01:00
dependabot[bot]
8853a942f9
Bump @types/node from 22.8.4 to 22.9.0 (#34663)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.8.4 to 22.9.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 16:52:39 +01:00
Steven Hawkins
927f110aef
fix: consolidating logic dealing with persisted property handling (#34260)
closes: #34258

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
2024-11-05 16:42:56 +01:00
pnzrr
bd1a5a1543
Change passwordResetBodyHtml key for remaining languages that hadn't translated the text for the reset link. Those were including a full link. All templates now have the same behavior with translated text. (#34642)
Closes: 34640

Signed-off-by: pnzrp2 <pnzr@phasetwo.io>
2024-11-05 14:26:45 +00:00
Jonas Suter
35b425736a Strip Double Quotes from Request Content in Organization API
Closes #34401

Signed-off-by: Jonas Suter <jonas_suter@gmx.ch>
2024-11-05 11:24:08 -03:00
Erik Jan de Wit
1718a3ee94
fixed link to documentation (#34613)
fixes: #34519

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-11-05 08:19:29 -05:00
Christian Ja
9851452be1
Restore the Cache tab in Realm Settings (#34311)
closes keycloak#17727

Signed-off-by: Christian Janker <christian.janker@gmx.at>
2024-11-05 14:09:35 +01:00
Christian Ja
6482e41cd8
Show forbidden section only after whoAmI is set (#34589)
closes #34402

Signed-off-by: Christian Janker <christian.janker@gmx.at>
2024-11-05 13:05:02 +01:00
Giuseppe Graziano
7d70ea7c20 Avoid continuous reload when KC_AUTH_SESSION_HASH expires
Closes #34652

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
2024-11-05 12:39:06 +01:00
dependabot[bot]
cd86405064
Bump eslint-plugin-react-compiler (#34645)
Bumps [eslint-plugin-react-compiler](https://github.com/facebook/react/tree/HEAD/compiler/packages/eslint-plugin-react-compiler) from 19.0.0-beta-9ee70a1-20241017 to 19.0.0-beta-63b359f-20241101.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/compiler/packages/eslint-plugin-react-compiler)

---
updated-dependencies:
- dependency-name: eslint-plugin-react-compiler
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 12:18:57 +01:00
dependabot[bot]
fd97f9c7d7
Bump typescript-eslint from 8.12.2 to 8.13.0 (#34646)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.12.2 to 8.13.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.13.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 12:18:44 +01:00
dependabot[bot]
8b1cdb1fc3
Bump rollup from 4.24.3 to 4.24.4 (#34647)
Bumps [rollup](https://github.com/rollup/rollup) from 4.24.3 to 4.24.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.24.3...v4.24.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-05 12:18:31 +01:00
Christian Ja
5b6ac5b14b
fix: Client Protocol Mappers with non UUID ids cannot be edited (#34643)
closes #34636

Signed-off-by: Christian Janker <christian.janker@gmx.at>
2024-11-05 11:29:37 +01:00
fwojnar
b3dd26a7c3
Migrate WelcomeTestPage to testsuite framework (#34543)
* Migrate WelcomeTestPage to testsuite framework

Closes #34491

Signed-off-by: wojnarfilip <fwojnar@redhat.com>

* Refactor welcome page a bit

Signed-off-by: stianst <stianst@gmail.com>

* Fixes for htmlunit

Signed-off-by: stianst <stianst@gmail.com>

* Cleanup imports

Signed-off-by: stianst <stianst@gmail.com>

---------

Signed-off-by: wojnarfilip <fwojnar@redhat.com>
Signed-off-by: stianst <stianst@gmail.com>
Co-authored-by: wojnarfilip <fwojnar@redhat.com>
Co-authored-by: stianst <stianst@gmail.com>
2024-11-05 10:57:58 +01:00
Stian Thorgersen
d6b01015c4
Database suppliers refactoring WIP (#34574)
Signed-off-by: stianst <stianst@gmail.com>
2024-11-05 07:20:11 +01:00
Giuseppe Graziano
612e2caae1 Refresh the login page when root auth session changes
Closes #32658

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
2024-11-04 18:31:42 +01:00
Alexander Schwartz
25e4995eb7 Fixing explicit Anchor for downstream
Closes #34634

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
2024-11-04 18:27:46 +01:00
Björn
cb38ad10ea
remove duplicate lines in theme
Closes #34614

Signed-off-by: bschumann <b.schumann@kasasi.de>
Co-authored-by: bschumann <b.schumann@kasasi.de>
2024-11-04 16:03:22 +00:00
Lukas Hanusovsky
440624e398
[Test framework] Update MVN dependencies (#34629)
Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
2024-11-04 15:27:42 +00:00
Alexander Schwartz
373656593d Fixing cross-references between guides
Closes #34624

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
2024-11-04 16:11:08 +01:00
Steven Hawkins
e8543e77d2
fix: ensure that kc.config.args is omitted from show-config (#34461)
closes: #34460

Signed-off-by: Steve Hawkins <shawkins@redhat.com>
2024-11-04 16:06:38 +01:00
Thomas Darimont
3315ea718a Add ability to enable OID4VCI Verifiable Credentials per realm (#34524)
- Added new realm property verifiableCredentialsEnabled
- Updated RealmRepresentation
- Guarded route to Oid4VCI page
- Add boolean switch to Realm settings page to control Verifiable Credentials enablement
- We now only show the Verifiable Credentials page in the nave if the "Verifiable Credentials" realm setting is enabled.

Fixes #34524

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
2024-11-04 14:58:30 +01:00
Douglas Palmer
f229790ba5 Allow custom message for brute force temporary lockout
Closes #17014

Signed-off-by: Douglas Palmer <dpalmer@redhat.com>
2024-11-04 14:49:32 +01:00
kqq
822d3fde32
Microsoft login - add prompt param configure
Closes #34583

Signed-off-by: kqq <971340511@qq.com>
Co-authored-by: kqq <971340511@qq.com>
2024-11-04 13:17:05 +01:00
Alexander Schwartz
8be4237fd4
Fix failing test AdminEventQueryTest by adding required session parameter (#34612)
Closes #34611

Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
2024-11-04 12:28:59 +01:00
dependabot[bot]
8855cf2316
Bump eslint from 9.13.0 to 9.14.0 (#34595)
Bumps [eslint](https://github.com/eslint/eslint) from 9.13.0 to 9.14.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.13.0...v9.14.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 09:52:17 +00:00
dependabot[bot]
f8df8e1c9a
Bump @faker-js/faker from 9.1.0 to 9.2.0 (#34597)
Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 9.1.0 to 9.2.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v9.1.0...v9.2.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 09:44:54 +00:00
Gilvan Filho
910caf5ff8
Update brute force docs
Fixes #27378

Signed-off-by: Gilvan Filho <gilvan.sfilho@gmail.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
2024-11-04 09:41:26 +00:00
dependabot[bot]
1a038af507
Bump tslib from 2.8.0 to 2.8.1 (#34556)
Bumps [tslib](https://github.com/Microsoft/tslib) from 2.8.0 to 2.8.1.
- [Release notes](https://github.com/Microsoft/tslib/releases)
- [Commits](https://github.com/Microsoft/tslib/compare/v2.8.0...v2.8.1)

---
updated-dependencies:
- dependency-name: tslib
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 10:35:58 +01:00
dependabot[bot]
07464b11de
Bump @testing-library/jest-dom from 6.6.2 to 6.6.3 (#34557)
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.6.2 to 6.6.3.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.6.2...v6.6.3)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 10:35:43 +01:00
dependabot[bot]
fb64e3ba5f
Bump @eslint/js from 9.13.0 to 9.14.0 (#34596)
Bumps [@eslint/js](https://github.com/eslint/eslint/tree/HEAD/packages/js) from 9.13.0 to 9.14.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/commits/v9.14.0/packages/js)

---
updated-dependencies:
- dependency-name: "@eslint/js"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 10:35:18 +01:00
dependabot[bot]
81950f5d17
Bump lightningcss from 1.27.0 to 1.28.1 (#34599)
Bumps [lightningcss](https://github.com/parcel-bundler/lightningcss) from 1.27.0 to 1.28.1.
- [Release notes](https://github.com/parcel-bundler/lightningcss/releases)
- [Commits](https://github.com/parcel-bundler/lightningcss/commits)

---
updated-dependencies:
- dependency-name: lightningcss
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-04 10:35:03 +01:00
Erik Jan de Wit
2b4fbfe66b
disable group test again (#34607)
related: #34605

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-11-04 09:29:54 +00:00
Erik Jan de Wit
e4101b1b61
changed to use TextArea (#34539)
fixes: #34201

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-11-04 10:23:40 +01:00
Bernd Bohmann
7681687e0a
Provide missing user event metrics from aerogear/keycloak-metrics-spi to a keycloak micrometer event listener
inspired by
https://github.com/aerogear/keycloak-metrics-spi
https://github.com/please-openit/keycloak-native-metrics

Closes #33043

Signed-off-by: Bernd Bohmann <bommel@apache.org>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Signed-off-by: Michal Hajas <mhajas@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Michal Hajas <mhajas@redhat.com>
2024-11-04 08:56:24 +01:00
mposolda
d80cb010ff Make documentation more clear that keycloak javascript adapter and node.js adapter are OIDC
closes #34570

Signed-off-by: mposolda <mposolda@gmail.com>
2024-11-04 08:44:46 +01:00
Stefan Guilhen
af434d6bc1 Add checks to prevent GroupLDAPStorageMapper from performing operations on groups it does not manage
Closes #11008
Closes #17593
Closes #19652

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
2024-11-01 15:49:55 -03:00
Stefan Guilhen
2e51775acc Remove Provider annotation along with default constructors from org resources
Closes #34335

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
2024-11-01 15:37:52 -03:00
Václav Muzikář
9a7cfb38ac
Fix Quarkus dev mode (#34550)
Closes #34549

Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
2024-11-01 14:57:00 +01:00
Stian Thorgersen
a7af380f71
Break up test-framework into multiple modules, and introduce placeholder for new testsuite (#34507)
Closes #34194

Signed-off-by: stianst <stianst@gmail.com>
2024-11-01 08:52:00 +01:00
dependabot[bot]
e72da1ac2c
Bump mocha from 10.8.1 to 10.8.2 (#34526)
Bumps [mocha](https://github.com/mochajs/mocha) from 10.8.1 to 10.8.2.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.8.1...v10.8.2)

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-31 22:08:41 +01:00
dependabot[bot]
53cfcdc273
Bump @patternfly/react-table from 5.4.8 to 5.4.9 (#34527)
Bumps [@patternfly/react-table](https://github.com/patternfly/patternfly-react) from 5.4.8 to 5.4.9.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-table@5.4.8...@patternfly/react-table@5.4.9)

---
updated-dependencies:
- dependency-name: "@patternfly/react-table"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-31 22:08:27 +01:00
dependabot[bot]
1d8b61b991
Bump eslint-plugin-cypress from 4.0.0 to 4.1.0 (#34528)
Bumps [eslint-plugin-cypress](https://github.com/cypress-io/eslint-plugin-cypress) from 4.0.0 to 4.1.0.
- [Release notes](https://github.com/cypress-io/eslint-plugin-cypress/releases)
- [Commits](https://github.com/cypress-io/eslint-plugin-cypress/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-cypress
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-31 22:08:11 +01:00
vramik
d853dcab7d Use specific error message from required actions for SamlProtocol if available
Closes #34514

Signed-off-by: vramik <vramik@redhat.com>
2024-10-31 15:45:19 -03:00
Thomas Darimont
36b01cbea0 Revise PAR request object parameter handlig (#34352)
We now store the original parameter value as-is, in case only a single parameter value is provided. In case multiple parameter values are provided
for the same parameter, we only retain the first parameter.
This ensures that the original value is retained. Previously the value list from the
`decodedFormParameters` `MultivaluedMap` was converted to a String while replacing '[' and ']'
with an empty string, which corrupted the original parameter values stored.

Fixes #34352

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
2024-10-31 16:26:31 +01:00
Ryan Emerson
ba51140a25 Asynchronously create EmbeddedCacheManager when JDBC_PING2 not required
Closes #34313

Signed-off-by: Ryan Emerson <remerson@redhat.com>
2024-10-31 12:55:15 +01:00
Max Hovens
4e540fa2a7
Remove inaccurate statement about master realm imports
This is supported since 26.0.0

Closes #34301

Signed-off-by: maxhov <14804474+maxhov@users.noreply.github.com>
2024-10-31 11:23:35 +00:00
Pedro Igor
db780ed6c7 Trying to make sure there is no active tasks and introduce a timeout
Closes #34432

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
2024-10-31 12:10:22 +01:00
Stefan Guilhen
9c50813bf4 Add validChecksum to jpa-changelog-26.0.0.xml
Closes #34450

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
2024-10-31 07:54:27 -03:00
rmartinc
78aa08941a Fix NPE in ConditionalOtpFormAuthenticator if no configuration
Closes #34298

Signed-off-by: rmartinc <rmartinc@redhat.com>
2024-10-31 07:48:07 -03:00
Ryan Emerson
a79b67cac8 Deprecate other transport stacks (ec2, azure, google)
Closes #34253

Signed-off-by: Ryan Emerson <remerson@redhat.com>
2024-10-31 11:47:13 +01:00
Erik Jan de Wit
19ef0a608b
Add switch to toggle dark mode (#33822)
Closes #33821

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Jon Koops <jonkoops@gmail.com>
Co-authored-by: Jon Koops <jonkoops@gmail.com>
2024-10-31 10:19:03 +00:00
Pedro Ruivo
0d9d2908f1
Username and password should be optional for multi-site deployment (#34511)
Fixes #34508

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
2024-10-31 10:47:41 +01:00
niekdonk
98a4faf289
Use property parameter instead of duplicating content
Use the provided pnpm.args.install property instead of duplicating the contents

Closes #34447

Signed-off-by: niekdonk <36667461+niekdonk@users.noreply.github.com>
2024-10-31 10:09:12 +01:00
Agnieszka Gancarczyk
c64e0ad583
Fixed persisting translations for attribute groups and improved errors for empty translations on attribute/attribute groups save (#33943)
* added fix for attribute groups

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* Improved translations for attributes

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improvement

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improvement

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improvement

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improvement

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improved fetching translations in NewAttributeSettings

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* improved fetching translations in NewAttributeSettings

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* cleanup

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

* cleanup

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>

---------

Signed-off-by: Agnieszka Gancarczyk <agagancarczyk@gmail.com>
2024-10-31 08:01:26 +01:00
Václav Muzikář
abb7c414ab
Remove not needed Quarkus dependencies related to Dev UI services (#34309)
Closes #34308

Signed-off-by: Václav Muzikář <vmuzikar@redhat.com>
2024-10-30 18:59:37 +00:00
Weblate (bot)
7e470e81f8
Updated translation for Japanese (#34448)
Language: ja

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Signed-off-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
2024-10-30 18:25:52 +00:00
dependabot[bot]
a76f9096e8
Bump rollup from 4.24.2 to 4.24.3 (#34473)
Bumps [rollup](https://github.com/rollup/rollup) from 4.24.2 to 4.24.3.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.24.2...v4.24.3)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 18:33:10 +01:00
Pedro Igor
4ad462fbd3 Do not rely on the pwdLastSet attribute when updating AD entries
Closes #34467

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
2024-10-30 17:43:07 +01:00
Stefan Guilhen
ac25844731 Ensure hide_on_login has the default value set to 0 on MSSQL
Closes #34450

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
2024-10-30 12:46:17 -03:00
vramik
b27a5d05b4 Fix error message in test
Signed-off-by: vramik <vramik@redhat.com>
2024-10-30 12:26:03 -03:00
Pedro Igor
f9f9a313b3 make sure error dialog is shown at the account console when declining terms
Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
35b109b4eb added missing message
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:26:03 -03:00
vramik
7368104e43 Keep error and error_description query params in login url.
Signed-off-by: vramik <vramik@redhat.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
77231bd68c always try and translate the error message
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:26:03 -03:00
vramik
3d91df42d8 Declining terms and conditions in account-console results in error
Closes #28328

Signed-off-by: vramik <vramik@redhat.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
8c2bc39418 added message
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
b4caeee0c7 * hide standard text when we have a description
* lookup description in message bundle

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
eb5afeeabb added description to denied consent and show on ErrorPage
fixes: #28328
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:26:03 -03:00
Erik Jan de Wit
fd2338c4fc
added table truncate on role description (#34289)
fixes: #32992

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 13:28:04 +01:00
Ryan Emerson
7152a8b0f3
Update caching docs to reflect that IP multicast is no longer used by default
Closes #34495

Signed-off-by: Ryan Emerson <remerson@redhat.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
2024-10-30 11:47:32 +00:00
dependabot[bot]
7bbc35cba7
Bump @playwright/test from 1.48.1 to 1.48.2 (#34372)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.48.1 to 1.48.2.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.48.1...v1.48.2)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 11:27:58 +00:00
dependabot[bot]
3c727a32f4
Bump @patternfly/react-code-editor from 5.4.10 to 5.4.11 (#34472)
Bumps [@patternfly/react-code-editor](https://github.com/patternfly/patternfly-react) from 5.4.10 to 5.4.11.
- [Release notes](https://github.com/patternfly/patternfly-react/releases)
- [Commits](https://github.com/patternfly/patternfly-react/compare/@patternfly/react-code-editor@5.4.10...@patternfly/react-code-editor@5.4.11)

---
updated-dependencies:
- dependency-name: "@patternfly/react-code-editor"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 11:24:49 +00:00
dependabot[bot]
617cadb84b
Bump @types/node from 22.8.2 to 22.8.4 (#34468)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.8.2 to 22.8.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 12:16:03 +01:00
dependabot[bot]
6af682a897
Bump typescript-eslint from 8.12.1 to 8.12.2 (#34469)
Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.12.1 to 8.12.2.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.12.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 12:15:43 +01:00
dependabot[bot]
87c87face7
Bump mocha from 10.7.3 to 10.8.1 (#34471)
Bumps [mocha](https://github.com/mochajs/mocha) from 10.7.3 to 10.8.1.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v10.7.3...v10.8.1)

---
updated-dependencies:
- dependency-name: mocha
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-30 12:15:28 +01:00
dependabot[bot]
97727dbed5
Bump @faker-js/faker from 9.0.3 to 9.1.0 (#34377)
* Bump @faker-js/faker from 9.0.3 to 9.1.0

Bumps [@faker-js/faker](https://github.com/faker-js/faker) from 9.0.3 to 9.1.0.
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v9.0.3...v9.1.0)

---
updated-dependencies:
- dependency-name: "@faker-js/faker"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* removed use of deprecated userName()

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2024-10-30 12:14:02 +01:00
Isaac Mercieca
64f97be053
fixes issue with labels not being applied for selected items defined as user profile attributes with type multiselect (#34457)
Signed-off-by: Isaac Mercieca <isaac.mercieca@rs2.com>
2024-10-30 10:51:09 +01:00
Thomas Darimont
e41ca1f579
Revise help icons for WebauthnPolicy settings (#34465) (#34466)
- Add missing icons with help labels
- Use correct help text for webAuthnPolicyCreateTimeout

Fixes #34465

Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
2024-10-30 10:32:14 +01:00
Giuseppe Graziano
3d663802bb Fix flaky test for concurrent client creation on H2 database
Closes #29290

Signed-off-by: Giuseppe Graziano <g.graziano94@gmail.com>
2024-10-29 20:58:50 -03:00
BrunoSampaioDTx
de973de800 Use the response_permissions_limit value, if provided, to set the maximum number of results when retrieving resources by URI
Signed-off-by: BrunoSampaioDTx <bruno.sampaio@dtx-colab.pt>
2024-10-29 16:40:44 -03:00
450 changed files with 5608 additions and 3400 deletions

View file

@ -7,7 +7,7 @@ inputs:
release-branches:
description: 'List of all related release branches (in JSON format)'
required: false
default: '["refs/heads/release/22.0","refs/heads/release/24.0"]'
default: '["refs/heads/release/22.0","refs/heads/release/24.0","refs/heads/release/26.0"]'
keep-days:
description: 'For how many days to store the particular artifact.'
required: false

View file

@ -24,9 +24,9 @@ runs:
with:
create-cache-if-it-doesnt-exist: true
- id: frontend-plugin-cache
name: Frontend Plugin Cache
uses: ./.github/actions/frontend-plugin-cache
- id: pnpm-store-cache
name: PNPM store cache
uses: ./.github/actions/pnpm-store-cache
- id: build-keycloak
name: Build Keycloak

View file

@ -22,9 +22,6 @@ outputs:
ci-webauthn:
description: Should "ci.yml" execute (WebAuthn)
value: ${{ steps.changes.outputs.ci-webauthn }}
ci-test-poc:
description: Should "ci.yml" execute (Test PoC)
value: ${{ steps.changes.outputs.ci-test-poc }}
operator:
description: Should "operator-ci.yml" execute
value: ${{ steps.changes.outputs.operator }}

View file

@ -60,5 +60,3 @@ js/libs/keycloak-js/ ci ci-quarkus
*.tsx codeql-typescript
testsuite::database-suite ci-store
test-poc/ ci ci-test-poc

View file

@ -0,0 +1,20 @@
name: Cache Cypress
description: Caches Cypress binary to speed up the build.
runs:
using: composite
steps:
- id: cache-key
name: Cache key based on Cypress version
shell: bash
run: echo "key=cypress-binary-$(jq -r '.devDependencies.cypress' js/apps/admin-ui/package.json)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Cache Cypress binary
with:
# See: https://docs.cypress.io/app/references/advanced-installation#Binary-cache
path: |
~/.cache/Cypress
/AppData/Local/Cypress/Cache
~/Library/Caches/Cypress
key: ${{ runner.os }}-${{ steps.cache-key.outputs.key }}

View file

@ -1,21 +0,0 @@
name: Frontend Plugin Cache
description: Caches NPM dependencies for the frontend-maven-plugin to speed up builds
runs:
using: composite
steps:
- name: Get PNPM version
id: pnpm-version
shell: bash
run: |
echo "version=$(./mvnw help:evaluate -Dexpression=pnpm.version -q -DforceStdout)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Cache PNPM store
with:
# See: https://pnpm.io/npmrc#store-dir
path: |
~/.local/share/pnpm/store
~/AppData/Local/pnpm/store
~/Library/pnpm/store
key: ${{ runner.os }}-frontend-plugin-pnpm-store-${{ steps.pnpm-version.outputs.version }}-${{ hashFiles('pnpm-lock.yaml') }}

View file

@ -25,9 +25,9 @@ runs:
name: Maven cache
uses: ./.github/actions/maven-cache
- id: frontend-plugin-cache
name: Frontend Plugin Cache
uses: ./.github/actions/frontend-plugin-cache
- id: pnpm-store-cache
name: PNPM store cache
uses: ./.github/actions/pnpm-store-cache
- id: download-keycloak
name: Download Keycloak Maven artifacts

View file

@ -20,26 +20,19 @@ runs:
shell: bash
run: corepack enable
- name: Get PNPM store directory
id: pnpm-cache
shell: bash
run: |
echo "store-path=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: PNPM store cache
uses: ./.github/actions/pnpm-store-cache
- uses: actions/cache@v4
name: Setup PNPM cache
with:
# Also cache Cypress binary.
path: |
~/.cache/Cypress
${{ steps.pnpm-cache.outputs.store-path }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cypress binary cache
uses: ./.github/actions/cypress-cache
- name: Install dependencies
shell: bash
# Run the store prune after the installation to avoid having caches which grow over time
run: |
pnpm install --prefer-offline --frozen-lockfile
pnpm store prune
run: pnpm install --prefer-offline --frozen-lockfile
# This step is only needed to ensure that the Cypress binary is installed.
# If the binary was retrieved from the cache, this step is a no-op.
- name: Install Cypress dependencies
shell: bash
working-directory: js/apps/admin-ui
run: pnpm exec cypress install

View file

@ -0,0 +1,20 @@
name: Cache PNPM store
description: Caches the PNPM store to speed up the build.
runs:
using: composite
steps:
- id: weekly-cache-key
name: Key for weekly rotation of cache
shell: bash
run: echo "key=pnpm-store-`date -u "+%Y-%U"`" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Cache PNPM store
with:
# See: https://pnpm.io/npmrc#store-dir
path: |
~/.local/share/pnpm/store
~/AppData/Local/pnpm/store
~/Library/pnpm/store
key: ${{ runner.os }}-${{ steps.weekly-cache-key.outputs.key }}

View file

@ -11,6 +11,6 @@ runs:
name: Maven cache
uses: ./.github/actions/maven-cache
- id: frontend-plugin-cache
- id: pnpm-store-cache
name: Frontend Plugin Cache
uses: ./.github/actions/frontend-plugin-cache
uses: ./.github/actions/pnpm-store-cache

View file

@ -33,7 +33,6 @@ jobs:
ci-store: ${{ steps.conditional.outputs.ci-store }}
ci-sssd: ${{ steps.conditional.outputs.ci-sssd }}
ci-webauthn: ${{ steps.conditional.outputs.ci-webauthn }}
ci-test-poc: ${{ steps.conditional.outputs.ci-test-poc }}
ci-aurora: ${{ steps.auroradb-tests.outputs.run-aurora-tests }}
steps:
@ -84,7 +83,7 @@ jobs:
run: |
SEP=""
PROJECTS=""
for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs|test-poc|test-framework)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do
for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs|tests|test-framework)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do
PROJECTS="$PROJECTS$SEP$i"
SEP=","
done
@ -958,7 +957,7 @@ jobs:
job-id: migration-tests-${{ matrix.old-version }}-${{ matrix.database }}
test-framework:
name: Keycloak Test Framework
name: Test Framework
runs-on: ubuntu-latest
needs: build
timeout-minutes: 30
@ -970,14 +969,12 @@ jobs:
uses: ./.github/actions/integration-test-setup
- name: Run tests
run: ./mvnw test -f test-framework/pom.xml
run: ./mvnw package -f test-framework/pom.xml
test-poc:
name: Test PoC
base-new-integration-tests:
name: Base IT (new)
runs-on: ubuntu-latest
if: needs.conditional.outputs.ci-test-poc == 'true'
needs:
- conditional
- build
timeout-minutes: 30
steps:
@ -988,9 +985,7 @@ jobs:
uses: ./.github/actions/integration-test-setup
- name: Run tests
env:
KC_TEST_BROWSER: chrome-headless
run: ./mvnw clean install -f test-poc/pom.xml
run: ./mvnw test -f tests/pom.xml
check:
name: Status Check - Keycloak CI
@ -1015,7 +1010,8 @@ jobs:
- sssd-unit-tests
- migration-tests
- external-infinispan-tests
- test-poc
- test-framework
- base-new-integration-tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View file

@ -240,7 +240,7 @@ jobs:
- name: Start Keycloak server
run: |
tar xfvz keycloak-999.0.0-SNAPSHOT.tar.gz
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz,transient-users &> ~/server.log &
keycloak-999.0.0-SNAPSHOT/bin/kc.sh start-dev --features=admin-fine-grained-authz:v1,transient-users &> ~/server.log &
env:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin

View file

@ -53,7 +53,9 @@ public class Profile {
ACCOUNT_V3("Account Console version 3", Type.DEFAULT, 3, Feature.ACCOUNT_API),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW, 1),
ADMIN_FINE_GRAINED_AUTHZ_V2("Fine-Grained Admin Permissions version 2", Type.EXPERIMENTAL, 2, Feature.AUTHORIZATION),
ADMIN_API("Admin API", Type.DEFAULT),
@ -123,6 +125,8 @@ public class Profile {
PASSKEYS("Passkeys", Type.PREVIEW),
CACHE_EMBEDDED_REMOTE_STORE("Support for remote-store in embedded Infinispan caches", Type.EXPERIMENTAL),
USER_EVENT_METRICS("Collect metrics based on user events", Type.PREVIEW),
;
private final Type type;
@ -337,13 +341,13 @@ public class Profile {
*/
private static Map<String, TreeSet<Feature>> getOrderedFeatures() {
if (FEATURES == null) {
// "natural" ordering low to high between two features
Comparator<Feature> comparator = Comparator.comparing(Feature::getType).thenComparingInt(Feature::getVersion);
// "natural" ordering low to high between two features (type has precedence and then reversed version is used)
Comparator<Feature> comparator = Comparator.comparing(Feature::getType).thenComparing(Comparator.comparingInt(Feature::getVersion).reversed());
// aggregate the features by unversioned key
HashMap<String, TreeSet<Feature>> features = new HashMap<>();
Stream.of(Feature.values()).forEach(f -> features.compute(f.getUnversionedKey(), (k, v) -> {
if (v == null) {
v = new TreeSet<>(comparator.reversed()); // we want the highest priority first
v = new TreeSet<>(comparator);
}
v.add(f);
return v;

View file

@ -35,4 +35,6 @@ public interface ServiceAccountConstants {
String CLIENT_HOST = "clientHost";
String CLIENT_ADDRESS = "clientAddress";
String SERVICE_ACCOUNT_SCOPE = "service_account";
}

View file

@ -29,7 +29,7 @@ public class ProfileTest {
private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.AUTHORIZATION;
private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER;
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ;
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.TOKEN_EXCHANGE;
private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES;
private static Profile.Feature DEPRECATED_FEATURE = Profile.Feature.LOGIN_V1;

View file

@ -218,6 +218,8 @@ public class RealmRepresentation {
protected Boolean organizationsEnabled;
private List<OrganizationRepresentation> organizations;
protected Boolean verifiableCredentialsEnabled;
@Deprecated
protected Boolean social;
@Deprecated
@ -1440,6 +1442,14 @@ public class RealmRepresentation {
this.organizationsEnabled = organizationsEnabled;
}
public Boolean isVerifiableCredentialsEnabled() {
return verifiableCredentialsEnabled;
}
public void setVerifiableCredentialsEnabled(Boolean verifiableCredentialsEnabled) {
this.verifiableCredentialsEnabled = verifiableCredentialsEnabled;
}
@JsonIgnore
public Map<String, String> getAttributesOrEmpty() {
return (Map<String, String>) (attributes == null ? Collections.emptyMap() : attributes);

View file

@ -8,3 +8,13 @@ If you are using a custom theme that extends any of the `keycloak` themes and ar
----
darkMode=false
----
Alternatively, you can disable dark mode support for the built-in Keycloak themes on a per-realm basis by turning off the "Dark mode" setting under the "Theme" tab in the realm settings.
= LDAP users are created as enabled by default when using Microsoft Active Directory
If you are using Microsoft AD and creating users through the administrative interfaces, the user will created as enabled by default.
In previous versions, it was only possible to update the user status after setting a (non-temporary) password to the user.
This behavior was not consistent with other built-in user storages as well as not consistent with others LDAP vendors supported
by the LDAP provider.

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 210 KiB

View file

@ -67,6 +67,7 @@ include::topics/threat.adoc[]
include::topics/threat/host.adoc[]
include::topics/threat/admin.adoc[]
include::topics/threat/brute-force.adoc[]
include::topics/threat/password.adoc[]
include::topics/threat/read-only-attributes.adoc[]
include::topics/threat/validate-user-attributes.adoc[]
include::topics/threat/clickjacking.adoc[]

View file

@ -2,68 +2,110 @@
[[password-guess-brute-force-attacks]]
=== Brute force attacks
A brute force attack attempts to guess a user's password by trying to log in multiple times. {project_name} has brute force detection capabilities and can temporarily disable a user account if the number of login failures exceeds a specified threshold.
A brute force attack attempts to guess a user's password by trying to log in multiple times. {project_name} has brute force detection capabilities and can permanently or temporarily disable a user account if the number of login failures exceeds a specified threshold.
[NOTE]
====
{project_name} disables brute force detection by default. Enable this feature to protect against brute force attacks.
When a user is locked and attempts to log in, {project_name} displays the default `Invalid username or password` error message. This message is the same error message as the message displayed for an invalid username or invalid password to ensure the attacker is unaware the account is disabled.
====
.Procedure
[WARNING]
====
Brute force detection is disabled by default. Enable this feature to protect against brute force attacks.
====
To enable this protection:
. Click *Realm Settings* in the menu
. Click the *Security Defenses* tab.
. Click the *Brute Force Detection* tab.
. Choose the *Brute Force Mode* which best fit to your requirements.
+
.Brute force detection
image:images/brute-force.png[]
{project_name} can deploy permanent lockout and temporary lockout actions when it detects an attack. Permanent lockout disables a user account until an administrator re-enables it. Temporary lockout disables a user account for a specific period of time.
The time period that the account is disabled increases as the attack continues and subsequent failures reach multiples of `Max Login Failures`.
==== Lockout permanently
{project_name} disables a user account (blocking log in attemps) until an administrator re-enables it.
[NOTE]
====
When a user is temporarily locked and attempts to log in, {project_name} displays the default `Invalid username or password` error message. This message is the same error message as the message displayed for an invalid username or invalid password to ensure the attacker is unaware the account is disabled.
====
.Lockout permanently
image:images/brute-force-permanently.png[]
*Common Parameters*
*Permanent Lockout Parameters*
|===
|Name |Description |Default
|Max Login Failures
|The maximum number of login failures.
|30 failures.
|30 failures
|Quick Login Check Milliseconds
|The minimum time between login attempts.
|1000 milliseconds.
|1000 milliseconds
|Minimum Quick Login Wait
|The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_.
|1 minute.
|1 minute
|===
*Permanent Lockout Flow*
====
. On successful login
.. Reset `count`
. On failed login
.. Increment `count`
.. If `count` is greater than or equals to `Max login failures`
... locks the user
.. Else if the time between this failure and the last failure is less than _Quick Login Check Milliseconds_
... Locks the user for the time specified at _Minimum Quick Login Wait_
====
[NOTE]
====
Enabling an user account resets the `count`.
====
==== Lockout temporarily
{project_name} disables a user account for a specific period of time. The time period that the account is disabled increases as the attack continues.
.Lockout temporarily
image:images/brute-force-temporarily.png[]
*Temporary Lockout Parameters*
|===
|Name |Description |Default
|Max Login Failures
|The maximum number of login failures.
|30 failures
|Strategy to increase wait time
|Strategy to increase the time a user will be temporarily disabled when the user's login attempts exceed _Max Login Failures_
|Multiple
|Wait Increment
|The time added to the time a user is temporarily disabled when the user's login attempts exceed _Max Login Failures_.
|1 minute.
|1 minute
|Max Wait
|The maximum time a user is temporarily disabled.
|15 minutes.
|15 minutes
|Failure Reset Time
|The time when the failure count resets. The timer runs from the last failed login. Make sure this number is always greater than `Max wait`; otherwise the effective
wait time will never reach the value you have set to `Max wait`.
|12 hours.
|12 hours
|Quick Login Check Milliseconds
|The minimum time between login attempts.
|1000 milliseconds
|Minimum Quick Login Wait
|The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_.
|1 minute
|===
@ -76,10 +118,15 @@ wait time will never reach the value you have set to `Max wait`.
... Reset `count`
.. Increment `count`
.. Calculate `wait` according the brute force strategy defined (see below Strategies to set Wait Time).
.. If `wait` equals is less than 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_, set `wait` to _Minimum Quick Login Wait_.
.. If `wait` is less than or equals to 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_
... set `wait` to _Minimum Quick Login Wait_
.. if `wait` is greater than 0
... Temporarily disable the user for the smallest of `wait` and _Max Wait_ seconds
... Increment the temporary lockout counter
====
[NOTE]
====
`count` does not increment when a temporarily disabled account commits a login failure.
====
@ -104,11 +151,11 @@ By multiples strategy, wait time is incremented when the number (or count) of fa
|**10** |**30** | 5 | **60**
|===
At the fifth failed attempt of the `Effective Wait Time`, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds.
At the fifth failed attempt, the account is disabled for `30` seconds. After reaching the next multiple of `Max Login Failures`, in this case `10`, the time increases from `30` to `60` seconds.
The By multiple strategy uses the following formula to calculate wait time: _Wait Increment_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number.
The By multiple strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (`count` / _Max Login Failures_). The division is an integer division rounded down to a whole number.
For linear strategy, wait time is incremented when the number (or count) of failures equals or is greater than `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` to`30` seconds, the effective time that an account is disabled after several failed authentication attempts will be:
For linear strategy, wait time is incremented when the `count` (or number) of failures is greater than or equals to `Max Login Failure`. For instance, if you have set `Max Login Failures` to `5` and a `Wait Increment` to`30` seconds, the effective time that an account is disabled after several failed authentication attempts will be:
[cols="1,1,1,1"]
|===
@ -125,33 +172,88 @@ For linear strategy, wait time is incremented when the number (or count) of fail
|**10** |**30** | 5 | **180**
|===
At the fifth failed attempt for the `Effective Wait Time`, the account is disabled for `30` seconds. Each new failed attempt increases wait time.
At the fifth failed attempt, the account is disabled for `30` seconds. Each new failure increases wait time according value specified at `wait increment`.
The linear strategy uses the following formula to calculate wait time: _Wait Increment_ * (1 + `count` - _Max Login Failures_).
The linear strategy uses the following formula to calculate wait time: _Wait Increment in Seconds_ * (1 + `count` - _Max Login Failures_).
*Permanent Lockout Parameters*
==== Lockout permanently after temporary lockout
Mixed mode. Locks user temporarily for specified number of times and then locks user permanently.
.Lockout permanently after temporary lockout
image:images/brute-force-mixed.png[]
*Permanent lockout after temporary lockouts Parameters*
|===
|Name |Description |Default
|Max temporary Lockouts
|Max Login Failures
|The maximum number of login failures.
|30 failures
|Maximum temporary Lockouts
|The maximum number of temporary lockouts permitted before permanent lockout occurs.
|0
|1
|Strategy to increase wait time
|Strategy to increase the time a user will be temporarily disabled when the user's login attempts exceed _Max Login Failures_
|Multiple
|Wait Increment
|The time added to the time a user is temporarily disabled when the user's login attempts exceed _Max Login Failures_.
|1 minute
|Max Wait
|The maximum time a user is temporarily disabled.
|15 minutes
|Failure Reset Time
|The time when the failure count resets. The timer runs from the last failed login. Make sure this number is always greater than `Max wait`; otherwise the effective
wait time will never reach the value you have set to `Max wait`.
|12 hours
|Quick Login Check Milliseconds
|The minimum time between login attempts.
|1000 milliseconds
|Minimum Quick Login Wait
|The minimum time the user is disabled when login attempts are quicker than _Quick Login Check Milliseconds_.
|1 minute
|===
*Permanent Lockout Flow*
*Permanent lockout after temporary lockouts Algorithm*
====
. Follow temporary lockout flow
. If temporary lockout counter exceeds Max temporary lockouts
.. Permanently disable user
. On successful login
.. Reset `count`
.. Reset `temporary lockout` counter
. On failed login
.. If the time between this failure and the last failure is greater than _Failure Reset Time_
... Reset `count`
... Reset `temporary lockout` counter
.. Increment `count`
.. Calculate `wait` according the brute force strategy defined (see below Strategies to set Wait Time).
.. If `wait` is less than or equals to 0 and the time between this failure and the last failure is less than _Quick Login Check Milliseconds_
... set `wait` to _Minimum Quick Login Wait_
... set `quick login failure` to `true``
.. if `wait` and `Maximum temporary Lockouts` is greater than 0
... set `wait` to the smallest of `wait` and _Max Wait_ seconds
.. if `quick login failure` is `false`
... Increment `temporary lockout` counter
.. If `temporary lockout` counter exceeds `Maximum temporary lockouts`
... Permanently locks the user
.. Else
... Temporarily blocks the user according `wait` value
When {project_name} disables a user, the user cannot log in until an administrator enables the user. Enabling an account resets the `count`.
====
[NOTE]
====
`count` does not increment when a temporarily disabled account commits a login failure.
====
==== Downside of {project_name} brute force detection
The downside of {project_name} brute force detection is that the server becomes vulnerable to denial of service attacks. When implementing a denial of service attack, an attacker can attempt to log in by guessing passwords for any accounts it knows and eventually causing {project_name} to disable the accounts.
Consider using intrusion prevention software (IPS). {project_name} logs every login failure and client IP address failure. You can point the IPS to the {project_name} server's log file, and the IPS can modify firewalls to block connections from these IP addresses.
==== Password policies
Ensure you have a complex password policy to force users to choose complex passwords. See the <<_password-policies, Password Policies>> chapter for more information. Prevent password guessing by setting up the {project_name} server to use one-time-passwords.

View file

@ -0,0 +1,4 @@
=== Password policies
Ensure you have a complex password policy to force users to choose complex passwords. See the <<_password-policies, Password Policies>> chapter for more information. Prevent password guessing by setting up the {project_name} server to use one-time-passwords.

View file

@ -22,7 +22,7 @@ These APIs are no longer needed as initialization is done automatically on deman
= Virtual Threads enabled for Infinispan and JGroups thread pools
Starting from this release, {project_name} automatically enables the virtual thread pool support in both the embedded Infinispan and JGroups when running on OpenJDK 21.
This removes the need to configure the thread pool and reduces overall memory footprint.
This removes the need to configure the JGroups thread pool, the need to align the JGroups thread pool with the HTTP worker thread pool, and reduces the overall memory footprint.
To disable the virtual threads, add one of the Java system properties combinations to your deployment:
* `-Dorg.infinispan.threads.virtual=false`: disables virtual thread in both Infinispan and JGroups.
@ -41,6 +41,11 @@ To enable the previous behavior, choose the transport stack `udp`.
The {project_name} Operator will continue to configure `kubernetes` as a transport stack.
= Deprecated transport stacks for distributed caches
The `azure`, `ec2` and `google` transport stacks have been deprecated. Users should use the TCP based `jdbc-ping`
stack as a direct replacement.
= Defining dependencies between provider factories
When developing extensions for {project_name}, developers can now specify dependencies between provider factories classes by implementing the method `dependsOn()` in the `ProviderFactory` interface.
@ -49,3 +54,15 @@ See the Javadoc for a detailed description.
= Removal of robots.txt file
The `robots.txt` file, previously included by default, is now removed. The default `robots.txt` file blocked all crawling, which prevented the `noindex`/`nofollow` directives from being followed. The desired default behaviour is for {project_name} pages to not show up in search engine results and this is accomplished by the existing `X-Robots-Tag` header, which is set to `none` by default. The value of this header can be overidden per-realm if a different behaviour is needed.
= Offline access removes the associated online session if the `offline_scope` is requested in the initial exchange
Any offline session in {project_name} is created from another online session. When the `offline_access` scope is requested, the current online session is used to create the associated offline session for the client. Therefore any `offline_access` request finished, until now, with two sessions, one online and one offline.
Starting with this version, {project_name} removes the initial online session if the `offline_scope` is directly requested as the first interaction for the session. The client retrieves the offline token after the code to token exchange that is associated to the offline session, but the previous online session is removed. If the online session has been used before the `offline_scope` request, by the same or another client, the online session remains active as today. Although the new behavior makes sense because the client application is just asking for an offline token, it can affect some scenarios that rely on having the online session still active after the initial `offline_scope` token request.
= New client scope `service_account` for `client_credentials` grant mappers
{project_name} introduces a new client scope at the realm level called `service_account` which is in charge of adding the specific claims for `client_credentials` grant (`client_id`, `clientHost` and `clientAddress`) via protocol mappers. This scope will be automatically assigned to and unassigned from the client when the `serviceAccountsEnabled` option is set or unset in the client configuration.
Previously, the three mappers (`Client Id`, `Client Host` and `Client IP Address`) where added directly to the dedicated scope when the client was configured to enable service accounts, and they were never removed.

View file

@ -12,6 +12,15 @@ For a configuration where this is applied, visit <@links.ha id="deploy-keycloak-
== Concepts
=== JGroups communications
// remove this paragraph once OpenJDK 17 is no longer supported on the server side.
// https://github.com/keycloak/keycloak/issues/31101
JGroups communications, which is used in single-site setups for the communication between {project_name} nodes, benefits from the use of virtual threads which are available in OpenJDK 21.
This reduces the memory usage and removes the need to configure thread pool sizes.
Therefore, the use of OpenJDK 21 is recommended.
=== Quarkus executor pool
{project_name} requests, as well as blocking probes, are handled by an executor pool. Depending on the available CPU cores, it has a maximum size of 50 or more threads.
@ -31,32 +40,6 @@ If you increase the number of database connections and the number of threads too
The number of database connections is configured via the link:{links_server_all-config_url}?q=db-pool[`Database` settings `db-pool-initial-size`, `db-pool-min-size` and `db-pool-max-size`] respectively.
Low numbers ensure fast response times for all clients, even if there is an occasionally failing request when there is a load spike.
=== JGroups connection pool
[NOTE]
====
* This currently applies to single-site setups only.
In a multi-site setup with an external {jdgserver_name} this is not a restriction.
* This currently applies if virtual threads are disabled.
Since {project_name} 26.1, virtual threads are enabled in both embedded Infinispan and JGroups if running on OpenJDK 21 or higher.
====
The combined number of executor threads in all {project_name} nodes in the cluster should not exceed too much the number of threads available in JGroups thread pool to avoid the warning `thread pool is full (max=<value>, active=<value>)`.
The warning includes a thread dump when the Java system property `-Djgroups.thread_dumps_enabled=true` is set.
It may incur in a penalty in performance collecting those thread dumps.
--
include::partials/threads/executor-jgroups-thread-calculation.adoc[]
--
Use metrics to monitor the total JGroup threads in the pool and for the threads active in the pool.
When using TCP as the JGroups transport protocol, the metrics `vendor_jgroups_tcp_get_thread_pool_size` and `vendor_jgroups_tcp_get_thread_pool_size_active` are available for monitoring. When using UDP, the metrics `vendor_jgroups_udp_get_thread_pool_size` and `vendor_jgroups_udp_get_thread_pool_size_active` are available.
This is useful to monitor that limiting the Quarkus thread pool size keeps the number of active JGroup threads below the maximum JGroup thread pool size.
WARNING: The metrics are not available when virtual threads are enabled in JGroups.
[#load-shedding]
=== Load Shedding

View file

@ -17,6 +17,7 @@ Use it together with the other building blocks outlined in the <@links.ha id="bb
* Understanding of a <@links.operator id="basic-deployment" /> of {project_name} with the {project_name} Operator.
* Aurora AWS database deployed using the <@links.ha id="deploy-aurora-multi-az" /> {section}.
* {jdgserver_name} server deployed using the <@links.ha id="deploy-infinispan-kubernetes-crossdc" /> {section}.
* Running {project_name} with OpenJDK 21, which is the default for the containers distributed for {project_name}, as this enabled virtual threads for the JGroups communication.
== Procedure
@ -46,8 +47,6 @@ See the <@links.ha id="concepts-database-connections" /> {section} for details.
<5> To be able to analyze the system under load, enable the metrics endpoint.
The disadvantage of the setting is that the metrics will be available at the external {project_name} endpoint, so you must add a filter so that the endpoint is not available from the outside.
Use a reverse proxy in front of {project_name} to filter out those URLs.
<6> You might consider limiting the number of {project_name} threads further because multiple concurrent threads will lead to throttling by Kubernetes once the requested CPU limit is reached.
See the <@links.ha id="concepts-threads" /> {section} for details.
== Verifying the deployment
@ -70,7 +69,11 @@ spec:
additionalOptions:
include::examples/generated/keycloak.yaml[tag=keycloak-queue-size]
----
All exceeding requests are served with an HTTP 503.
You might consider limiting the value for `http-pool-max-threads` further because multiple concurrent threads will lead to throttling by Kubernetes once the requested CPU limit is reached.
See the <@links.ha id="concepts-threads" /> {section} about load shedding for details.
== Optional: Disable sticky sessions

View file

@ -464,16 +464,16 @@ spec:
- name: http-metrics-slos
value: '5,10,25,50,250,500'
# tag::keycloak[]
# end::keycloak[]
# tag::keycloak-queue-size[]
- name: http-max-queued-requests
value: "1000"
# end::keycloak-queue-size[]
# tag::keycloak[]
- name: log-console-output
value: json
- name: metrics-enabled # <5>
value: 'true'
- name: http-pool-max-threads # <6>
value: "200"
# tag::keycloak-ispn[]
- name: cache-remote-host # <1>
value: "infinispan.keycloak.svc"

View file

@ -453,16 +453,16 @@ spec:
- name: http-metrics-slos
value: '5,10,25,50,250,500'
# tag::keycloak[]
# end::keycloak[]
# tag::keycloak-queue-size[]
- name: http-max-queued-requests
value: "1000"
# end::keycloak-queue-size[]
# tag::keycloak[]
- name: log-console-output
value: json
- name: metrics-enabled # <5>
value: 'true'
- name: http-pool-max-threads # <6>
value: "66"
# end::keycloak[]
# This block is just for documentation purposes as we need both versions of Infinispan config, with and without numbers to corresponding options
# tag::keycloak[]
@ -510,6 +510,7 @@ spec:
- name: JAVA_OPTS_APPEND # <5>
value: ""
ports:
# end::keycloak[]
# readinessProbe:
# exec:
# command:

View file

@ -1,5 +0,0 @@
The number of JGroup threads is `200` by default.
While it can be configured using the property Java system property `jgroups.thread_pool.max_threads`, we advise keeping it at this value.
As shown in experiments, the total number of Quarkus worker threads in the cluster should not exceed the number of threads in the JGroup thread pool of `200` in each node to avoid requests being dropped in the JGroups communication.
Given a {project_name} cluster with four nodes, each node should then have around 50 Quarkus worker threads.
Use the {project_name} configuration option `http-pool-max-threads` to configure the maximum number of Quarkus worker threads.

View file

@ -21,6 +21,7 @@ The built-in supported `providers` are:
The following sections will describe how to use the different providers.
[#_authentication]
== Authentication
To invoke the Client Registration Services you usually need a token. The token can be a bearer token, an initial access token or a registration access token.

View file

@ -7,6 +7,7 @@ priority=30
summary="Client-side JavaScript library that can be used to secure web applications.">
{project_name} comes with a client-side JavaScript library called `keycloak-js` that can be used to secure web applications. The adapter also comes with built-in support for Cordova applications.
The adapter uses OpenID Connect protocol under the covers. You can take a look at the <@links.securingapps id="oidc-layers" anchor="_oidc_available_endpoints"/> {section} for the more generic information about OpenID Connect endpoints and capabilities.
== Installation

View file

@ -7,6 +7,7 @@ priority=40
summary="Node.js adapter to protect server-side JavaScript apps">
{project_name} provides a Node.js adapter built on top of https://github.com/senchalabs/connect[Connect] to protect server-side JavaScript apps - the goal was to be flexible enough to integrate with frameworks like https://expressjs.com/[Express.js].
The adapter uses OpenID Connect protocol under the covers. You can take a look at the <@links.securingapps id="oidc-layers" anchor="_oidc_available_endpoints"/> {section} for the more generic information about OpenID Connect endpoints and capabilities.
ifeval::[{project_community}==true]
The library can be downloaded directly from https://www.npmjs.com/package/keycloak-connect[ {project_name} organization] and the source is available at

View file

@ -6,7 +6,7 @@ title="Secure applications and services with OpenID Connect"
priority=20
summary="Using OpenID Connect with Keycloak to secure applications and services">
<#include "partials/oidc/available-endpoints.adoc" />
include::partials/oidc/available-endpoints.adoc[]
include::partials/oidc/supported-grant-types.adoc[]

View file

@ -1,3 +1,5 @@
[#_oidc_available_endpoints]
== Available Endpoints
As a fully-compliant OpenID Connect Provider implementation, {project_name} exposes a set of endpoints that applications

View file

@ -14,7 +14,8 @@ The current distributed cache implementation is built on top of https://infinisp
== Enable distributed caching
When you start {project_name} in production mode, by using the `start` command, caching is enabled and all {project_name} nodes in your network are discovered.
By default, caches are using a UDP transport stack so that nodes are discovered using IP multicast transport based on UDP. For most production environments, there are better discovery alternatives to UDP available. {project_name} allows you to either choose from a set of pre-defined default transport stacks, or to define your own custom stack, as you will see later in this {section}.
By default, caches use the `jdbc-ping-udp` stack which is based upon a UDP transport and uses the configured database to track nodes joining the cluster.
{project_name} allows you to either choose from a set of pre-defined default transport stacks, or to define your own custom stack, as you will see later in this {section}.
To explicitly enable distributed infinispan caching, enter this command:
@ -246,6 +247,10 @@ The following table shows transport stacks that are available using the `--cache
|===
=== Additional transport stacks
IMPORTANT: The following stacks are deprecated. We recommend that you utilise the `jdbc-ping` stack in such environments
as it does not require additional configuration or dependencies.
The following table shows transport stacks that are supported by {project_name}, but need some extra steps to work.
Note that _none_ of these stacks are Kubernetes / OpenShift stacks, so no need exists to enable the `google` stack if you want to run {project_name} on top of the Google Kubernetes engine.
In that case, use the `kubernetes` stack.

View file

@ -76,6 +76,9 @@ The table below summarizes the available metrics groups:
|Cache
|A set of metrics from Infinispan caches. See <@links.server id="caching"/> for more details.
|Keycloak
|A set of metrics from Keycloak events. See <@links.server id="event-metrics"/> for more details.
|===
</@tmpl.guide>

View file

@ -0,0 +1,60 @@
<#import "/templates/guide.adoc" as tmpl>
<#import "/templates/kc.adoc" as kc>
<#import "/templates/options.adoc" as opts>
<#import "/templates/links.adoc" as links>
<@tmpl.guide
title="Enabling {project_name} Event Metrics"
summary="Learn how to enable and use {project_name} Event Metrics"
preview="true"
includedOptions="metrics-enabled event-metrics-user-*">
Event metrics can provide admins an overview of the different activities in a {project_name} instance.
For now, only metrics for user events are captured.
For example, you can monitor the number of logins, login failures, or token refreshes performed.
The metrics are exposed using the standard metrics endpoint, and you can use it in your own metrics collection system to create dashboards and alerts.
The metrics are reported as counters per {project_name} instance.
The counters are reset on the restart of the instance.
If you have multiple instances running in a cluster, you will need to collect the metrics from all instances and aggregate them to get per a cluster view.
== Enable event metrics
To start collecting metrics, enable the feature `user-event-metrics`, enable metrics, and enable the metrics for user events.
The following shows the required startup parameters:
<@kc.start parameters="--features=user-event-metrics --metrics-enabled=true --event-metrics-user-enabled=true ..."/>
By default, there is a separate metric for each realm.
To break down the metric by client and identity provider, you can add those metrics dimension using the configuration option `event-metrics-user-tags`.
This can be useful on installations with a small number of clients and IDPs.
This is not recommended for installations with a large number of clients or IDPs as it will increase the memory usage of {project_name} and as it will increase the load on your monitoring system.
The following shows how to configure {project_name} to break down the metrics by all three metrics dimensions:
<@kc.start parameters="... --event-metrics-user-tags=realm,idp,clientId ..."/>
You can limit the events for which {project_name} will expose metrics.
The following example limits the events collected to `LOGIN` and `LOGOUT` events:
<@kc.start parameters="... --event-metrics-user-events=login,logout ..."/>
All error events will be collected with the primary event type and will have the `error` tag filled with the error code.
The snippet below is an example of a response provided by the metric endpoint:
[source]
----
# HELP keycloak_user_events_total Keycloak user events
# TYPE keycloak_user_events_total counter
keycloak_user_events_total{client_id="security-admin-console",error="",event="code_to_token",idp="",realm="master",} 1.0
keycloak_user_events_total{client_id="security-admin-console",error="",event="login",idp="",realm="master",} 1.0
keycloak_user_events_total{client_id="security-admin-console",error="",event="logout",idp="",realm="master",} 1.0
keycloak_user_events_total{client_id="security-admin-console",error="invalid_user_credentials",event="login",idp="",realm="master",} 1.0
----
</@tmpl.guide>

View file

@ -130,8 +130,6 @@ realms and potentially lose state between server restarts.
To re-create realms you should explicitly run the `import` command prior to starting the server.
Importing the `master` realm is not supported because as it is a very sensitive operation.
== Importing and Exporting by using the Admin Console
You can also import and export a realm using the Admin Console. This functionality is

View file

@ -18,6 +18,7 @@ fips
management-interface
health
configuration-metrics
event-metrics
tracing
importExport
vault

View file

@ -42,7 +42,6 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyCriteria;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;

View file

@ -569,6 +569,11 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
return Collections.emptyList();
}
if (!isGroupInGroupPath(realm, kcGroup)) {
// group being inspected is not managed by this mapper - return empty collection
return Collections.emptyList();
}
// TODO: with ranged search in AD we can improve the search using the specific range (not done for the moment)
LDAPObject ldapGroup = loadLDAPGroupByName(kcGroup.getName());
if (ldapGroup == null) {
@ -703,18 +708,18 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
@Override
public Stream<GroupModel> getGroupsStream() {
Stream<GroupModel> ldapGroupMappings = getLDAPGroupMappingsConverted();
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
if (config.isTopLevelGroupsPath() && config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
// Use just group mappings from LDAP
return ldapGroupMappings;
} else {
// Merge mappings from both DB and LDAP
// Merge mappings from both DB and LDAP (including groups assigned from other group mappers)
return Stream.concat(ldapGroupMappings, super.getGroupsStream());
}
}
@Override
public void joinGroup(GroupModel group) {
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY) {
if (config.getMode() == LDAPGroupMapperMode.LDAP_ONLY && isGroupInGroupPath(realm, group)) {
// We need to create new role mappings in LDAP
cachedLDAPGroupMappings = null;
addGroupMappingInLDAP(realm, group, ldapUser);
@ -725,6 +730,11 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
@Override
public void leaveGroup(GroupModel group) {
// if user is leaving group not managed by this mapper, let the call proceed to the next mapper or to the DB.
if (!isGroupInGroupPath(realm, group)) {
super.leaveGroup(group);
}
try (LDAPQuery ldapQuery = createGroupQuery(true)) {
LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder();
Condition roleNameCondition = conditionsBuilder.equal(config.getGroupNameLdapAttribute(), group.getName());
@ -756,7 +766,7 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
@Override
public boolean isMemberOf(GroupModel group) {
return RoleUtils.isDirectMember(getGroupsStream(),group);
return isGroupInGroupPath(realm, group) && RoleUtils.isDirectMember(getGroupsStream(),group);
}
protected Stream<GroupModel> getLDAPGroupMappingsConverted() {
@ -795,6 +805,23 @@ public class GroupLDAPStorageMapper extends AbstractLDAPStorageMapper implements
return config.isTopLevelGroupsPath() ? null : KeycloakModelUtils.findGroupByPath(session, realm, config.getGroupsPath());
}
protected boolean isGroupInGroupPath(RealmModel realm, GroupModel group) {
if (config.isTopLevelGroupsPath()) {
return true; // any group is in the path of the top level path.
}
GroupModel groupPathGroup = KeycloakModelUtils.findGroupByPath(session, realm, config.getGroupsPath());
if (groupPathGroup != null) {
while(!groupPathGroup.getId().equals(group.getId())) {
group = group.getParent();
if (group == null) {
return false; // we checked every ancestor group, and none matches the group path group.
}
}
return true;
}
return false;
}
/**
* Creates a new KC group from given LDAP group name in given KC parent group or the groups path.
*/

View file

@ -248,7 +248,7 @@ public class MSADUserAccountControlStorageMapper extends AbstractLDAPStorageMapp
@Override
public void setEnabled(boolean enabled) {
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && getPwdLastSet() > 0) {
if (UserStorageProvider.EditMode.WRITABLE.equals(ldapProvider.getEditMode())) {
MSADUserAccountControlStorageMapper.logger.debugf("Going to propagate enabled=%s for ldapUser '%s' to MSAD", enabled, ldapUser.getDn().toString());
UserAccountControl control = getUserAccountControl(ldapUser);

View file

@ -5,7 +5,7 @@
<base href="${resourceUrl}/">
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light${(properties.darkMode)?boolean?then(' dark', '')}">
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
<meta name="description" content="${properties.description!'The Account Console is a web-based interface for managing your account.'}">
<title>${properties.title!'Account Management'}</title>
<style>
@ -58,7 +58,7 @@
}
}
</script>
<#if properties.darkMode?boolean>
<#if darkMode>
<script type="module" async blocking="render">
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

View file

@ -75,7 +75,6 @@ linkedAccounts=Linked accounts
personalInfoDescription=Manage your basic information
removeAccess=Remove access
signingInDescription=Configure ways to sign in.
somethingWentWrongDescription=Sorry, an unexpected error has occurred.
personalInfo=Personal info
removeCred=Remove {{name}}
signOutAllDevices=Sign out all devices
@ -98,10 +97,11 @@ permissionRequest=Permission requests - {{name}}
add=Add
error-invalid-value='{{0}}' has invalid value.
somethingWentWrong=Something went wrong
somethingWentWrongDescription=Sorry, an unexpected error has occurred.
tryAgain=Try again
rolesScope=If there is no role scope mapping defined, each user is permitted to use this client scope. If there are role scope mappings defined, the user must be a member of at least one of the roles.
unShareError=Could not un-share the resource due to\: {{error}}
ipAddress=IP address
tryAgain=Try again
resourceName=Resource name
unlinkedEmpty=No unlinked providers
done=Done
@ -214,3 +214,4 @@ searchOrganization=Search for organization
organizationList=List of organizations
domains=Domains
refresh=Refresh
termsAndConditionsDeclined=You need to accept the Terms and Conditions to continue

View file

@ -28,7 +28,7 @@
"@patternfly/patternfly": "^5.4.1",
"@patternfly/react-core": "^5.4.8",
"@patternfly/react-icons": "^5.4.2",
"@patternfly/react-table": "^5.4.8",
"@patternfly/react-table": "^5.4.9",
"i18next": "^23.16.4",
"i18next-http-backend": "^2.6.2",
"keycloak-js": "workspace:*",
@ -41,12 +41,12 @@
},
"devDependencies": {
"@keycloak/keycloak-admin-client": "workspace:*",
"@playwright/test": "^1.48.1",
"@playwright/test": "^1.48.2",
"@types/lodash-es": "^4.17.12",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react-swc": "^3.7.1",
"lightningcss": "^1.27.0",
"lightningcss": "^1.28.1",
"vite": "^5.4.10",
"vite-plugin-checker": "^0.8.0",
"vite-plugin-dts": "^4.3.0"

View file

@ -1,5 +1,4 @@
export { PersonalInfo } from "./personal-info/PersonalInfo";
export { ErrorPage } from "./root/ErrorPage";
export { Header } from "./root/Header";
export { PageNav } from "./root/PageNav";
export { DeviceActivity } from "./account-security/DeviceActivity";

View file

@ -1,65 +0,0 @@
import {
Button,
Modal,
ModalVariant,
Page,
Text,
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { isRouteErrorResponse, useRouteError } from "react-router-dom";
type ErrorPageProps = {
error?: unknown;
};
export const ErrorPage = (props: ErrorPageProps) => {
const { t } = useTranslation();
const error = useRouteError() ?? props.error;
const errorMessage = getErrorMessage(error);
function onRetry() {
location.href = location.origin + location.pathname;
}
return (
<Page>
<Modal
variant={ModalVariant.small}
title={t("somethingWentWrong")}
titleIconVariant="danger"
showClose={false}
isOpen
actions={[
<Button key="tryAgain" variant="primary" onClick={onRetry}>
{t("tryAgain")}
</Button>,
]}
>
<TextContent>
<Text>{t("somethingWentWrongDescription")}</Text>
{errorMessage && (
<Text component={TextVariants.small}>{errorMessage}</Text>
)}
</TextContent>
</Modal>
</Page>
);
};
function getErrorMessage(error: unknown): string | null {
if (typeof error === "string") {
return error;
}
if (isRouteErrorResponse(error)) {
return error.statusText;
}
if (error instanceof Error) {
return error.message;
}
return null;
}

View file

@ -1,10 +1,9 @@
import { lazy } from "react";
import type { IndexRouteObject, RouteObject } from "react-router-dom";
import { environment } from "./environment";
import { Organizations } from "./organizations/Organizations";
import { ErrorPage } from "./root/ErrorPage";
import { Root } from "./root/Root";
import { ErrorPage } from "@keycloak/keycloak-ui-shared";
const DeviceActivity = lazy(() => import("./account-security/DeviceActivity"));
const LinkedAccounts = lazy(() => import("./account-security/LinkedAccounts"));
@ -85,7 +84,7 @@ export const RootRoute: RouteObject = {
PersonalInfoRoute,
ResourcesRoute,
ContentRoute,
Oid4VciRoute,
...(environment.features.isOid4VciEnabled ? [Oid4VciRoute] : []),
],
};

View file

@ -119,7 +119,7 @@ describe("Group test", () => {
.assertNoSearchResultsMessageExist(true);
});
it("Duplicate group", () => {
it.skip("Duplicate group from item bar", () => {
groupPage
.duplicateGroupItem(groupNames[0], true)
.assertNotificationGroupDuplicated();

View file

@ -74,7 +74,7 @@ export default class RoleMappingTab {
selectRow(name: string, modal = false) {
cy.get(modal ? ".pf-v5-c-modal-box " : "" + this.#namesColumn)
.contains(name)
.parent()
.parents("tr")
.within(() => {
cy.get("input").click();
});

View file

@ -21,7 +21,7 @@ export default class AssociatedRolesPage {
cy.get(this.#addRoleTable)
.contains(roleName)
.parent()
.parents("tr")
.within(() => {
cy.get("input").click();
});
@ -49,7 +49,7 @@ export default class AssociatedRolesPage {
cy.get(this.#addRoleTable)
.contains(roleName)
.parent()
.parents("tr")
.within(() => {
cy.get("input").click();
});
@ -67,7 +67,7 @@ export default class AssociatedRolesPage {
cy.get(this.#addRoleTable)
.contains(roleName)
.parent()
.parents("tr")
.within(() => {
cy.get("input").click();
});

View file

@ -24,13 +24,13 @@ export default class RealmSettingsPage extends CommonPage {
userProfileTab = "rs-user-profile-tab";
tokensTab = "rs-tokens-tab";
selectLoginTheme = "#kc-login-theme";
loginThemeList = "[data-testid='select-login-theme']";
loginThemeList = "[data-testid='select-loginTheme']";
selectAccountTheme = "#kc-account-theme";
accountThemeList = "[data-testid='select-account-theme']";
accountThemeList = "[data-testid='select-accountTheme']";
selectAdminTheme = "#kc-admin-ui-theme";
adminThemeList = "[data-testid='select-admin-theme']";
adminThemeList = "[data-testid='select-adminTheme']";
selectEmailTheme = "#kc-email-theme";
emailThemeList = "[data-testid='select-email-theme']";
emailThemeList = "[data-testid='select-emailTheme']";
ssoSessionIdleSelectMenu = "#kc-sso-session-idle-select-menu";
ssoSessionIdleSelectMenuList = "#kc-sso-session-idle-select-menu ul";
ssoSessionMaxSelectMenu = "#kc-sso-session-max-select-menu";

View file

@ -40,7 +40,7 @@ export default class UserRegistration {
selectRow(name: string) {
cy.get(this.#namesColumn)
.contains(name)
.parent()
.parents("tr")
.within(() => {
cy.get("input").click();
});

View file

@ -591,7 +591,6 @@ hour=時
connectionTimeoutHelp=LDAP接続タイムアウトミリ秒単位
defaultSigAlgHelp=このレルムでトークンの署名に使用されるデフォルトのアルゴリズム
save-admin-eventsHelp=有効の場合は、管理イベントがデータベースに保存され、管理コンソールで使用可能になります。
policyGroups=どのユーザーがこのポリシーで許可されるか指定してください。
forwardParametersHelp=最初のアプリケーションへのリクエストから取得し、外部IDPの認可エンドポイントへ転送されるOpenID Connect/OAuth標準以外のクエリー・パラメーター。複数のパラメーターをカンマ,)で区切って入力できます。
on=オン
webAuthnPolicyRpId=リライング・パーティー・エンティティーID
@ -804,3 +803,4 @@ resourceNameHelp=このリソースの一意な名前。この名前は、リソ
duplicateEmailsAllowed=メールの重複
policyClientHelp=このポリシーで許可されるクライアントを指定します。
clientAuthenticatorTypeHelp=Keycloakサーバーに対してこのクライアントの認証に使用するクライアント認証方式を設定します。
policyGroupsHelp=どのユーザーがこのポリシーで許可されるか指定してください。

View file

@ -5,7 +5,7 @@
<base href="${resourceUrl}/">
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light${(properties.darkMode)?boolean?then(' dark', '')}">
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
<meta name="description" content="${properties.description!'The Keycloak Administration Console is a web-based interface for managing Keycloak.'}">
<title>${properties.title!'Keycloak Administration Console'}</title>
<style>
@ -15,6 +15,8 @@
body, #app {
height: 100%;
overflow-x: hidden;
overflow-y: hidden;
}
.container {
@ -58,7 +60,7 @@
}
}
</script>
<#if properties.darkMode?boolean>
<#if darkMode>
<script type="module" async blocking="render">
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

View file

@ -231,7 +231,7 @@ eventTypes.USER_DISABLED_BY_TEMPORARY_LOCKOUT_ERROR.name=User disabled by tempor
deleteUser=Delete user
addedNodeSuccess=Node successfully added
eventTypes.INTROSPECT_TOKEN_ERROR.description=Introspect token error
webAuthnPolicyUserVerificationRequirementHelp=Communicates to an authenticator to confirm actually verifying a user.
webAuthnPolicyUserVerificationRequirementHelp=Communicates to an authenticator whether to require to verify a user.
syncModes.import=Import
realmSaveError=Realm could not be updated\: {{error}}
authDataDescription=Represents a token carrying authorization data as a result of the processing of an authorization request. This representation is basically what Keycloak issues to clients asking for permission. Check the `authorization` claim for the permissions that where granted based on the current authorization request.
@ -418,7 +418,7 @@ x509CertificateHelp=X509 Certificate encoded in PEM format
samlEndpointsLabel=SAML 2.0 Service Provider Metadata
passCurrentLocaleHelp=Pass the current locale to the identity provider as a ui_locales parameter.
lessThan=Must be less than {{value}}
webAuthnPolicyRequireResidentKeyHelp=It tells an authenticator create a public key credential as Discoverable Credential or not.
webAuthnPolicyRequireResidentKeyHelp=It tells an authenticator whether to create a public key credential as a Discoverable Credential.
logoutServiceRedirectBindingURL=Logout Service Redirect Binding URL
createIdentityProviderSuccess=Identity provider successfully created
emptyMappersInstructions=If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.
@ -689,7 +689,7 @@ clientPolicySearch=Search client policy
refreshTokens=Refresh tokens
eventTypes.UPDATE_EMAIL_ERROR.description=Update email error
credentials=Credentials
webAuthnPolicyCreateTimeoutHelp=Timeout value for creating user's public key credential in seconds. if set to 0, this timeout option is not adapted.
webAuthnPolicyCreateTimeoutHelp=The timeout value for creating the user's public key credential in seconds. If set to 0, this timeout option is not adapted.
policyType.hotp=Counter based
claimFilterValue=Essential claim value
eventTypes.REGISTER_ERROR.name=Register error
@ -1250,7 +1250,7 @@ realmRoles=Realm roles
fineGrainOpenIdConnectConfigurationHelp=This section is used to configure advanced settings of this client related to OpenID Connect protocol.
searchForUserDescription=This realm may have a federated provider. Viewing all users may cause the system to slow down, but it can be done by searching for "*". Please search for a user above.
expirationHelp=Sets the expiration for events. Expired events are periodically deleted from the database.
webAuthnPolicySignatureAlgorithmsHelp=What signature algorithms should be used for Authentication Assertion.
webAuthnPolicySignatureAlgorithmsHelp=The signature algorithms that should be used for the Authentication Assertion.
setToNowError=Error\! Failed to set notBefore to current date and time: {{error}}
eventTypes.UNREGISTER_NODE_ERROR.description=Unregister node error
clientScopeTypes.optional=Optional
@ -1272,7 +1272,7 @@ revoke=Revoke
admin=Admin
syncUsersError=Could not sync users\: '{{error}}'
generatedAccessTokenHelp=See the example access token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself
webAuthnPolicyAcceptableAaguidsHelp=The list of AAGUID of which an authenticator can be registered.
webAuthnPolicyAcceptableAaguidsHelp=The list of allowed AAGUIDs of which an authenticator can be registered. An AAGUID is a 128-bit identifier indicating the authenticator's type (e.g., make and model).
keyPasswordHelp=Password for the private key
frontchannelLogout=Front channel logout
clientUpdaterTrustedHostsTooltip=List of Hosts, which are trusted. In case that client registration/update request comes from the host/domain specified in this configuration, condition evaluates to true. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com' ) then whole domain example.com will be trusted.
@ -1721,7 +1721,7 @@ mappedGroupAttributes=Mapped group attributes
localization=Localization
importConfig=Import config from file
replyToDisplayNameHelp=A user-friendly name for the 'Reply-To' address (optional).
webAuthnPolicyRpIdHelp=This is ID as WebAuthn Relying Party. It must be origin's effective domain.
webAuthnPolicyRpIdHelp=The WebAuthn Relying Party ID (RpID). It must be the origin's effective domain, e.g. 'company.com' or 'auth.company.com'.
signingKeysConfigExplain=If you enable the "Client signature required" below, you must configure the signing keys by generating or importing keys, and the client will sign their saml requests and responses. The signature will be validated.
newClientProfile=Create client profile
consoleDisplayConnectionUrlHelp=Connection URL to your LDAP server
@ -2853,7 +2853,7 @@ credentialData=Data
clientRolesConditionTooltip=Client roles, which will be checked during this condition evaluation. Condition evaluates to true if client has at least one client role with the name as the client roles specified in the configuration.
invalidateSecret=Invalidate
emptyPermissionInstructions=If you want to create a permission, please click the button below to create a resource-based or scope-based permission.
webAuthnPolicyAvoidSameAuthenticatorRegisterHelp=Avoid registering the authenticator that has already been registered.
webAuthnPolicyAvoidSameAuthenticatorRegisterHelp=Avoid registering an authenticator that has already been registered.
memberofLdapAttribute=Member-of LDAP attribute
supportedLocales=Supported locales
showPasswordDataValue=Value
@ -2936,7 +2936,7 @@ clientSecretHelp=The client secret registered with the identity provider. This f
offlineSessionMax=Offline Session Max
generatedUserInfoHelp=See the example User Info, which will be provided by the User Info Endpoint
dynamicScopeFormat=Dynamic scope format
webAuthnPolicyExtraOriginsHelp=The list of extra origin for non-web application.
webAuthnPolicyExtraOriginsHelp=The list of extra origins for non-web applications.
updatePermissionSuccess=Successfully updated the permission
idpLinkSuccess=Identity provider has been linked
removeAnnotationText=Remove annotation
@ -3165,6 +3165,8 @@ logo=Logo
avatarImage=Avatar image
organizationsEnabled=Organizations
organizationsEnabledHelp=If enabled, allows managing organizations. Otherwise, existing organizations are still kept but you will not be able to manage them anymore or authenticate their members.
verifiableCredentialsEnabled=Verifiable Credentials
verifiableCredentialsEnabledHelp=If enabled, allows managing verifiable credentials in this realm.
organizations=Organizations
organizationDetails=Organization details
organizationsList=Organizations
@ -3273,7 +3275,24 @@ groupDuplicated=Group duplicated
duplicateAGroup=Duplicate group
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
darkModeEnabled=Dark mode
darkModeEnabledHelp=If enabled the dark variant of the theme will be applied based on user preference through an operating system setting (e.g. light or dark mode) or a user agent setting, if disabled only the light variant will be used. This setting only applies to themes that support dark and light variants, on themes that do not support this feature it will have no effect.
showMemberships=Show memberships
showMembershipsTitle={{username}} Group Memberships
noGroupMembershipsText=This user is not a member of any groups.
noGroupMemberships=No memberships
termsAndConditionsDeclined=You need to accept the Terms and Conditions to continue
somethingWentWrong=Something went wrong
somethingWentWrongDescription=Sorry, an unexpected error has occurred.
tryAgain=Try again
errorSavingTranslations=Error saving translations\: '{{error}}'
clearCachesTitle=Clear Caches
realmCache=Realm Cache
userCache=User Cache
keysCache=Keys Cache
clearButtonTitle=Clear
clearRealmCacheHelp=This will clear entries for all realms.
clearUserCacheHelp=This will clear entries for all realms.
clearKeysCacheHelp=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. This will clear all entries for all realms.
clearCacheSuccess=Cache cleared successfully
clearCacheError=Could not clear cache\: {{error}}

View file

@ -74,11 +74,11 @@
"@keycloak/keycloak-admin-client": "workspace:*",
"@keycloak/keycloak-ui-shared": "workspace:*",
"@patternfly/patternfly": "^5.4.1",
"@patternfly/react-code-editor": "^5.4.10",
"@patternfly/react-code-editor": "^5.4.11",
"@patternfly/react-core": "^5.4.8",
"@patternfly/react-icons": "^5.4.2",
"@patternfly/react-styles": "^5.4.1",
"@patternfly/react-table": "^5.4.8",
"@patternfly/react-table": "^5.4.9",
"admin-ui": "file:",
"dagre": "^0.8.5",
"file-saver": "^2.0.5",
@ -101,7 +101,7 @@
"@4tw/cypress-drag-drop": "^2.2.5",
"@testing-library/cypress": "^10.0.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.0.1",
"@types/dagre": "^0.7.52",
"@types/file-saver": "^2.0.7",
@ -110,12 +110,12 @@
"@types/react-dom": "^18.3.1",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react-swc": "^3.7.1",
"cypress": "^13.15.1",
"cypress": "^13.15.2",
"cypress-axe": "^1.5.0",
"cypress-split": "^1.24.5",
"jsdom": "^25.0.1",
"ldap-server-mock": "^6.0.1",
"lightningcss": "^1.27.0",
"lightningcss": "^1.28.1",
"ts-node": "^10.9.2",
"uuid": "^11.0.2",
"vite": "^5.4.10",

View file

@ -23,6 +23,9 @@ import { HelpHeader } from "./components/help-enabler/HelpHeader";
import { useRealm } from "./context/realm-context/RealmContext";
import { useWhoAmI } from "./context/whoami/WhoAmI";
import { toDashboard } from "./dashboard/routes/Dashboard";
import useToggle from "./utils/useToggle";
import { PageHeaderClearCachesModal } from "./PageHeaderClearCachesModal";
import { useAccess } from "./context/access/Access";
const ManageAccountDropdownItem = () => {
const { keycloak } = useEnvironment();
@ -67,6 +70,20 @@ const ServerInfoDropdownItem = () => {
);
};
const ClearCachesDropdownItem = () => {
const { t } = useTranslation();
const [open, toggleModal] = useToggle();
return (
<>
<DropdownItem key="clear caches" onClick={() => toggleModal()}>
{t("clearCachesTitle")}
</DropdownItem>
{open && <PageHeaderClearCachesModal onClose={() => toggleModal()} />}
</>
);
};
const HelpDropdownItem = () => {
const { t } = useTranslation();
const { enabled, toggleHelp } = useHelp();
@ -81,23 +98,34 @@ const HelpDropdownItem = () => {
);
};
const kebabDropdownItems = [
const kebabDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
<ManageAccountDropdownItem key="kebab Manage Account" />,
<ServerInfoDropdownItem key="kebab Server Info" />,
...(isMasterRealm && isManager
? [<ClearCachesDropdownItem key="Clear Caches" />]
: []),
<HelpDropdownItem key="kebab Help" />,
<Divider component="li" key="kebab sign out separator" />,
<SignOutDropdownItem key="kebab Sign out" />,
];
const userDropdownItems = [
const userDropdownItems = (isMasterRealm: boolean, isManager: boolean) => [
<ManageAccountDropdownItem key="Manage Account" />,
<ServerInfoDropdownItem key="Server info" />,
...(isMasterRealm && isManager
? [<ClearCachesDropdownItem key="Clear Caches" />]
: []),
<Divider component="li" key="sign out separator" />,
<SignOutDropdownItem key="Sign out" />,
];
const KebabDropdown = () => {
const [isDropdownOpen, setDropdownOpen] = useState(false);
const { realm } = useRealm();
const { hasAccess } = useAccess();
const isMasterRealm = realm === "master";
const isManager = hasAccess("manage-realm");
return (
<Dropdown
@ -116,7 +144,9 @@ const KebabDropdown = () => {
)}
isOpen={isDropdownOpen}
>
<DropdownList>{kebabDropdownItems}</DropdownList>
<DropdownList>
{kebabDropdownItems(isMasterRealm, isManager)}
</DropdownList>
</Dropdown>
);
};
@ -124,6 +154,11 @@ const KebabDropdown = () => {
const UserDropdown = () => {
const { whoAmI } = useWhoAmI();
const [isDropdownOpen, setDropdownOpen] = useState(false);
const { realm } = useRealm();
const { hasAccess } = useAccess();
const isMasterRealm = realm === "master";
const isManager = hasAccess("manage-realm");
return (
<Dropdown
@ -140,7 +175,7 @@ const UserDropdown = () => {
</MenuToggle>
)}
>
<DropdownList>{userDropdownItems}</DropdownList>
<DropdownList>{userDropdownItems(isMasterRealm, isManager)}</DropdownList>
</Dropdown>
);
};

View file

@ -0,0 +1,101 @@
import {
AlertVariant,
Button,
Flex,
FlexItem,
List,
ListItem,
Modal,
ModalVariant,
} from "@patternfly/react-core";
import { useRealm } from "./context/realm-context/RealmContext";
import { useAdminClient } from "./admin-client";
import { useTranslation } from "react-i18next";
import { HelpItem, useAlerts } from "@keycloak/keycloak-ui-shared";
export type ClearCachesModalProps = {
onClose: () => void;
};
export const PageHeaderClearCachesModal = ({
onClose,
}: ClearCachesModalProps) => {
const { realm: realmName } = useRealm();
const { t } = useTranslation();
const { adminClient } = useAdminClient();
const { addError, addAlert } = useAlerts();
const clearCache =
(clearCacheFn: typeof adminClient.cache.clearRealmCache) =>
async (realm: string) => {
try {
await clearCacheFn({ realm });
addAlert(t("clearCacheSuccess"), AlertVariant.success);
} catch (error) {
addError("clearCacheError", error);
}
};
const clearRealmCache = clearCache(adminClient.cache.clearRealmCache);
const clearUserCache = clearCache(adminClient.cache.clearUserCache);
const clearKeysCache = clearCache(adminClient.cache.clearKeysCache);
return (
<Modal
title={t("clearCachesTitle")}
variant={ModalVariant.small}
isOpen
onClose={onClose}
onClick={(e) => e.stopPropagation()}
>
<List isPlain isBordered>
<ListItem>
<Flex justifyContent={{ default: "justifyContentSpaceBetween" }}>
<FlexItem>
{t("realmCache")}{" "}
<HelpItem
helpText={t("clearRealmCacheHelp")}
fieldLabelId="clearRealmCacheHelp"
/>
</FlexItem>
<FlexItem>
<Button onClick={() => clearRealmCache(realmName)}>
{t("clearButtonTitle")}
</Button>
</FlexItem>
</Flex>
</ListItem>
<ListItem>
<Flex justifyContent={{ default: "justifyContentSpaceBetween" }}>
<FlexItem>
{t("userCache")}{" "}
<HelpItem
helpText={t("clearUserCacheHelp")}
fieldLabelId="clearUserCacheHelp"
/>
</FlexItem>
<FlexItem>
<Button onClick={() => clearUserCache(realmName)}>
{t("clearButtonTitle")}
</Button>
</FlexItem>
</Flex>
</ListItem>
<ListItem>
<Flex justifyContent={{ default: "justifyContentSpaceBetween" }}>
<FlexItem>
{t("keysCache")}{" "}
<HelpItem
helpText={t("clearKeysCacheHelp")}
fieldLabelId="clearKeysCacheHelp"
/>
</FlexItem>
<FlexItem>
<Button onClick={() => clearKeysCache(realmName)}>
{t("clearButtonTitle")}
</Button>
</FlexItem>
</Flex>
</ListItem>
</List>
</Modal>
);
};

View file

@ -66,6 +66,7 @@ const USER_VERIFY = [
type WeauthnSelectProps = {
name: string;
label: string;
labelIcon?: string;
options: readonly string[];
labelPrefix?: string;
isMultiSelect?: boolean;
@ -74,6 +75,7 @@ type WeauthnSelectProps = {
const WebauthnSelect = ({
name,
label,
labelIcon,
options,
labelPrefix,
isMultiSelect = false,
@ -82,7 +84,8 @@ const WebauthnSelect = ({
return (
<SelectControl
name={name}
label={t(label)}
label={label}
labelIcon={labelIcon}
variant={isMultiSelect ? "typeaheadMulti" : "single"}
controller={{ defaultValue: options[0] }}
options={options.map((option) => ({
@ -165,7 +168,8 @@ export const WebauthnPolicy = ({
/>
<WebauthnSelect
name={`${namePrefix}SignatureAlgorithms`}
label="webAuthnPolicySignatureAlgorithms"
label={t("webAuthnPolicySignatureAlgorithms")}
labelIcon={t("webAuthnPolicySignatureAlgorithmsHelp")}
options={SIGNATURE_ALGORITHMS}
isMultiSelect
/>
@ -176,32 +180,36 @@ export const WebauthnPolicy = ({
/>
<WebauthnSelect
name={`${namePrefix}AttestationConveyancePreference`}
label="webAuthnPolicyAttestationConveyancePreference"
label={t("webAuthnPolicyAttestationConveyancePreference")}
labelIcon={t("webAuthnPolicyAttestationConveyancePreferenceHelp")}
options={ATTESTATION_PREFERENCE}
labelPrefix="attestationPreference"
/>
<WebauthnSelect
name={`${namePrefix}AuthenticatorAttachment`}
label="webAuthnPolicyAuthenticatorAttachment"
label={t("webAuthnPolicyAuthenticatorAttachment")}
labelIcon={t("webAuthnPolicyAuthenticatorAttachmentHelp")}
options={AUTHENTICATOR_ATTACHMENT}
labelPrefix="authenticatorAttachment"
/>
<WebauthnSelect
name={`${namePrefix}RequireResidentKey`}
label="webAuthnPolicyRequireResidentKey"
label={t("webAuthnPolicyRequireResidentKey")}
labelIcon={t("webAuthnPolicyRequireResidentKeyHelp")}
options={RESIDENT_KEY_OPTIONS}
labelPrefix="residentKey"
/>
<WebauthnSelect
name={`${namePrefix}UserVerificationRequirement`}
label="webAuthnPolicyUserVerificationRequirement"
label={t("webAuthnPolicyUserVerificationRequirement")}
labelIcon={t("webAuthnPolicyUserVerificationRequirementHelp")}
options={USER_VERIFY}
labelPrefix="userVerify"
/>
<TimeSelectorControl
name={`${namePrefix}CreateTimeout`}
label={t("webAuthnPolicyCreateTimeout")}
labelIcon={t("otpPolicyPeriodHelp")}
labelIcon={t("webAuthnPolicyCreateTimeoutHelp")}
units={["second", "minute", "hour"]}
controller={{
defaultValue: 0,

View file

@ -185,6 +185,7 @@ export default function EditClientScope() {
realm,
id: clientScope!.id!,
mapperId: mapper.id!,
viewMode: "new",
}),
);
} else {
@ -256,7 +257,12 @@ export default function EditClientScope() {
onAdd={addMappers}
onDelete={onDelete}
detailLink={(id) =>
toMapper({ realm, id: clientScope.id!, mapperId: id! })
toMapper({
realm,
id: clientScope.id!,
mapperId: id!,
viewMode: "edit",
})
}
/>
</Tab>

View file

@ -34,7 +34,7 @@ export default function MappingDetails() {
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const { id, mapperId } = useParams<MapperParams>();
const { id, mapperId, viewMode } = useParams<MapperParams>();
const form = useForm();
const { setValue, handleSubmit } = form;
const [mapping, setMapping] = useState<ProtocolMapperTypeRepresentation>();
@ -46,8 +46,7 @@ export default function MappingDetails() {
const navigate = useNavigate();
const { realm } = useRealm();
const serverInfo = useServerInfo();
const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/;
const isUpdating = !!isGuid.exec(mapperId);
const isUpdating = viewMode === "edit";
const isOnClientScope = !!useMatch(MapperRoute.path);
const toDetails = () =>

View file

@ -7,12 +7,13 @@ export type MapperParams = {
realm: string;
id: string;
mapperId: string;
viewMode: "edit" | "new";
};
const MappingDetails = lazy(() => import("../details/MappingDetails"));
export const MapperRoute: AppRouteObject = {
path: "/:realm/client-scopes/:id/mappers/:mapperId",
path: "/:realm/client-scopes/:id/mappers/:mapperId/:viewMode",
element: <MappingDetails />,
breadcrumb: (t) => t("mappingDetails"),
handle: {

View file

@ -7,6 +7,7 @@ export type MapperParams = {
realm: string;
id: string;
mapperId: string;
viewMode: "edit" | "new";
};
const MappingDetails = lazy(
@ -14,7 +15,7 @@ const MappingDetails = lazy(
);
export const MapperRoute: AppRouteObject = {
path: "/:realm/clients/:id/clientScopes/dedicated/mappers/:mapperId",
path: "/:realm/clients/:id/clientScopes/dedicated/mappers/:mapperId/:viewMode",
element: <MappingDetails />,
breadcrumb: (t) => t("mappingDetails"),
handle: {

View file

@ -60,6 +60,7 @@ export default function DedicatedScopes() {
realm,
id: client.id!,
mapperId: mapper.id!,
viewMode: "new",
}),
);
} else {
@ -122,7 +123,7 @@ export default function DedicatedScopes() {
onAdd={addMappers}
onDelete={onDeleteMapper}
detailLink={(mapperId) =>
toMapper({ realm, id: client.id!, mapperId })
toMapper({ realm, id: client.id!, mapperId, viewMode: "edit" })
}
/>
</Tab>

View file

@ -1,3 +1,4 @@
import { useHelp } from "@keycloak/keycloak-ui-shared";
import {
Divider,
Dropdown,
@ -7,14 +8,12 @@ import {
Split,
SplitItem,
Switch,
TextContent,
} from "@patternfly/react-core";
import { ExternalLinkAltIcon, HelpIcon } from "@patternfly/react-icons";
import { HelpIcon } from "@patternfly/react-icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import helpUrls from "../../help-urls";
import { useHelp } from "@keycloak/keycloak-ui-shared";
import { FormattedLink } from "../external-link/FormattedLink";
import "./help-header.css";
@ -24,21 +23,14 @@ export const HelpHeader = () => {
const { t } = useTranslation();
const dropdownItems = [
<DropdownItem
key="link"
id="link"
<DropdownItem key="link" id="link">
<FormattedLink
href={helpUrls.documentationUrl}
target="_blank"
>
<Split>
<SplitItem isFilled>{t("documentation")}</SplitItem>
<SplitItem>
<ExternalLinkAltIcon />
</SplitItem>
</Split>
title={t("documentation")}
/>
</DropdownItem>,
<Divider key="divide" />,
<DropdownItem key="enable" id="enable">
<DropdownItem key="enable" id="enable" description={t("helpToggleInfo")}>
<Split>
<SplitItem isFilled>{t("enableHelpMode")}</SplitItem>
<SplitItem>
@ -52,9 +44,6 @@ export const HelpHeader = () => {
/>
</SplitItem>
</Split>
<TextContent className="keycloak_help-header-description">
{t("helpToggleInfo")}
</TextContent>
</DropdownItem>,
];
return (

View file

@ -1,3 +1,8 @@
import RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import {
KeycloakDataTable,
ListEmptyState,
} from "@keycloak/keycloak-ui-shared";
import {
Button,
Dropdown,
@ -9,14 +14,13 @@ import {
ToolbarItem,
} from "@patternfly/react-core";
import { FilterIcon } from "@patternfly/react-icons";
import { cellWidth, TableText } from "@patternfly/react-table";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client";
import { useAccess } from "../../context/access/Access";
import { translationFormatter } from "../../utils/translationFormatter";
import useLocaleSort from "../../utils/useLocaleSort";
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
import { KeycloakDataTable } from "@keycloak/keycloak-ui-shared";
import { ResourcesKey, Row, ServiceRole } from "./RoleMapping";
import { getAvailableRoles } from "./queries";
import { getAvailableClientRoles } from "./resource";
@ -33,6 +37,15 @@ type AddRoleMappingModalProps = {
type FilterType = "roles" | "clients";
const RoleDescription = ({ role }: { role: RoleRepresentation }) => {
const { t } = useTranslation();
return (
<TableText wrapModifier="truncate">
{translationFormatter(t)(role.description) as string}
</TableText>
);
};
export const AddRoleMappingModal = ({
id,
name,
@ -184,11 +197,12 @@ export const AddRoleMappingModal = ({
{
name: "name",
cellRenderer: ServiceRole,
transforms: [cellWidth(20)],
},
{
name: "role.description",
displayKey: "description",
cellFormatters: [translationFormatter(t)],
cellRenderer: RoleDescription,
},
]}
emptyState={

View file

@ -79,6 +79,10 @@ export class WhoAmI {
public isTemporary(): boolean {
return this.#me?.temporary ?? false;
}
public isEmpty(): boolean {
return !this.#me;
}
}
type WhoAmIProps = {

View file

@ -92,9 +92,9 @@ const Fields = ({ readOnly }: DiscoverySettingsProps) => {
/>
) : (
<>
<TextControl
<TextAreaControl
name="config.publicKeySignatureVerifier"
label="validatingPublicKey"
label={t("validatingPublicKey")}
/>
<TextControl
name="config.publicKeySignatureVerifierKeyId"

View file

@ -227,6 +227,13 @@ function RealmSettingsGeneralTabForm({
labelIcon={t("organizationsEnabledHelp")}
/>
)}
{isOpenid4vciEnabled && (
<DefaultSwitchControl
name="verifiableCredentialsEnabled"
label={t("verifiableCredentialsEnabled")}
labelIcon={t("verifiableCredentialsEnabledHelp")}
/>
)}
<SelectControl
name="unmanagedAttributePolicy"
label={t("unmanagedAttributes")}
@ -266,7 +273,7 @@ function RealmSettingsGeneralTabForm({
title={t("samlIdentityProviderMetadata")}
/>
</StackItem>
{isOpenid4vciEnabled && (
{isOpenid4vciEnabled && realm.verifiableCredentialsEnabled && (
<StackItem>
<FormattedLink
href={`${addTrailingSlash(

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import type {
UserProfileAttribute,
UserProfileConfig,
@ -61,6 +62,7 @@ type UserProfileAttributeFormFields = Omit<
annotations: IndexedAnnotations[];
hasSelector: boolean;
hasRequiredScopes: boolean;
translations?: TranslationForm[];
};
type Attribute = {
@ -172,7 +174,8 @@ export default function NewAttributeSettings() {
useFetch(
async () => {
const translationsToSave: any[] = [];
const translationsToSave: Translations[] = [];
await Promise.all(
combinedLocales.map(async (selectedLocale) => {
try {
@ -183,55 +186,50 @@ export default function NewAttributeSettings() {
});
const formData = form.getValues();
const formattedKey = formData.displayName?.substring(
const formattedKey =
formData.displayName?.substring(
2,
formData.displayName.length - 1,
);
const filteredTranslations: Array<{
locale: string;
value: string;
}> = [];
const allTranslations = Object.entries(translations).map(
([key, value]) => ({
key,
value,
}),
);
) || "";
allTranslations.forEach((translation) => {
if (translation.key === formattedKey) {
filteredTranslations.push({
const filteredTranslations: TranslationForm[] = Object.entries(
translations,
)
.filter(([key]) => key === formattedKey)
.map(([_, value]) => ({
locale: selectedLocale,
value: translation.value,
});
}
});
value,
}));
const translationToSave: any = {
if (filteredTranslations.length > 0) {
translationsToSave.push({
key: formattedKey,
translations: filteredTranslations,
};
translationsToSave.push(translationToSave);
});
}
} catch (error) {
console.error(
`Error fetching translations for ${selectedLocale}:`,
error,
);
addError("errorSavingTranslations", error);
}
}),
);
return translationsToSave;
},
(translationsToSaveData) => {
setTranslationsData(() => ({
key: translationsToSaveData[0].key,
translations: translationsToSaveData.flatMap(
(translationData) => translationData.translations,
),
}));
(translationsToSave) => {
if (translationsToSave && translationsToSave.length > 0) {
const allTranslations = translationsToSave.flatMap(
(translation) => translation.translations,
);
setTranslationsData({
key: translationsToSave[0].key,
translations: allTranslations,
});
form.setValue("translations", allTranslations);
}
},
[combinedLocales],
[combinedLocales, realmName, form],
);
useFetch(
@ -282,8 +280,9 @@ export default function NewAttributeSettings() {
const saveTranslations = async () => {
try {
const nonEmptyTranslations = translationsData.translations.map(
async (translation) => {
const nonEmptyTranslations = translationsData.translations
.filter((translation) => translation.value.trim() !== "")
.map(async (translation) => {
try {
await adminClient.realms.addLocalization(
{
@ -293,11 +292,11 @@ export default function NewAttributeSettings() {
},
translation.value,
);
} catch {
console.error(`Error saving translation for ${translation.locale}`);
} catch (error) {
addError(t("errorSavingTranslations"), error);
}
},
);
});
await Promise.all(nonEmptyTranslations);
} catch (error) {
console.error(`Error saving translations: ${error}`);
@ -377,7 +376,7 @@ export default function NewAttributeSettings() {
(translation) => translation.value.trim() !== "",
);
if (!hasNonEmptyTranslations && !formFields.displayName) {
if (!hasNonEmptyTranslations) {
addError("createAttributeError", t("translationError"));
return;
}

View file

@ -1,20 +1,11 @@
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
import {
HelpItem,
KeycloakSelect,
SelectVariant,
} from "@keycloak/keycloak-ui-shared";
import {
ActionGroup,
Button,
FormGroup,
PageSection,
SelectOption,
} from "@patternfly/react-core";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { SelectControl } from "@keycloak/keycloak-ui-shared";
import { ActionGroup, Button, PageSection } from "@patternfly/react-core";
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { FormAccess } from "../components/form/FormAccess";
import { DefaultSwitchControl } from "../components/SwitchControl";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { convertToFormValues } from "../util";
@ -29,12 +20,8 @@ export const RealmSettingsThemesTab = ({
}: RealmSettingsThemesTabProps) => {
const { t } = useTranslation();
const [loginThemeOpen, setLoginThemeOpen] = useState(false);
const [accountThemeOpen, setAccountThemeOpen] = useState(false);
const [adminUIThemeOpen, setAdminUIThemeOpen] = useState(false);
const [emailThemeOpen, setEmailThemeOpen] = useState(false);
const { control, handleSubmit, setValue } = useForm<RealmRepresentation>();
const form = useForm<RealmRepresentation>();
const { handleSubmit, setValue } = form;
const themeTypes = useServerInfo().themes!;
const setupForm = () => {
@ -42,6 +29,11 @@ export const RealmSettingsThemesTab = ({
};
useEffect(setupForm, []);
const appendEmptyChoice = (items: { key: string; value: string }[]) => [
{ key: "", value: t("choose") },
...items,
];
return (
<PageSection variant="light">
<FormAccess
@ -50,178 +42,70 @@ export const RealmSettingsThemesTab = ({
className="pf-v5-u-mt-lg"
onSubmit={handleSubmit(save)}
>
<FormGroup
label={t("loginTheme")}
fieldId="kc-login-theme"
labelIcon={
<HelpItem
helpText={t("loginThemeHelp")}
fieldLabelId="loginTheme"
<FormProvider {...form}>
<DefaultSwitchControl
name="attributes.darkMode"
labelIcon={t("darkModeEnabledHelp")}
label={t("darkModeEnabled")}
defaultValue="true"
stringify
/>
}
>
<Controller
<SelectControl
id="kc-login-theme"
name="loginTheme"
control={control}
defaultValue=""
render={({ field }) => (
<KeycloakSelect
toggleId="kc-login-theme"
onToggle={() => setLoginThemeOpen(!loginThemeOpen)}
onSelect={(value) => {
field.onChange(value as string);
setLoginThemeOpen(false);
}}
selections={field.value}
variant={SelectVariant.single}
isOpen={loginThemeOpen}
placeholderText={t("selectATheme")}
data-testid="select-login-theme"
aria-label={t("selectLoginTheme")}
>
{themeTypes.login.map((theme, idx) => (
<SelectOption
selected={theme.name === field.value}
key={`login-theme-${idx}`}
value={theme.name}
>
{theme.name}
</SelectOption>
))}
</KeycloakSelect>
label={t("loginTheme")}
labelIcon={t("loginThemeHelp")}
controller={{ defaultValue: "" }}
options={appendEmptyChoice(
themeTypes.login.map((theme) => ({
key: theme.name,
value: theme.name,
})),
)}
/>
</FormGroup>
<FormGroup
label={t("accountTheme")}
fieldId="kc-account-theme"
labelIcon={
<HelpItem
helpText={t("accountThemeHelp")}
fieldLabelId="accountTheme"
/>
}
>
<Controller
<SelectControl
id="kc-account-theme"
name="accountTheme"
control={control}
defaultValue=""
render={({ field }) => (
<KeycloakSelect
toggleId="kc-account-theme"
onToggle={() => setAccountThemeOpen(!accountThemeOpen)}
onSelect={(value) => {
field.onChange(value as string);
setAccountThemeOpen(false);
}}
selections={field.value}
variant={SelectVariant.single}
aria-label={t("selectAccountTheme")}
isOpen={accountThemeOpen}
label={t("accountTheme")}
labelIcon={t("accountThemeHelp")}
placeholderText={t("selectATheme")}
data-testid="select-account-theme"
>
{themeTypes.account
.filter((theme) => theme.name !== "base")
.map((theme, idx) => (
<SelectOption
selected={theme.name === field.value}
key={`account-theme-${idx}`}
value={theme.name}
>
{t(theme.name)}
</SelectOption>
))}
</KeycloakSelect>
controller={{ defaultValue: "" }}
options={appendEmptyChoice(
themeTypes.account.map((theme) => ({
key: theme.name,
value: theme.name,
})),
)}
/>
</FormGroup>
<FormGroup
label={t("adminTheme")}
fieldId="kc-admin-ui-theme"
labelIcon={
<HelpItem
helpText={t("adminThemeHelp")}
fieldLabelId="adminTheme"
/>
}
>
<Controller
<SelectControl
id="kc-admin-theme"
name="adminTheme"
control={control}
defaultValue=""
render={({ field }) => (
<KeycloakSelect
toggleId="kc-admin-ui-theme"
onToggle={() => setAdminUIThemeOpen(!adminUIThemeOpen)}
onSelect={(value) => {
field.onChange(value as string);
setAdminUIThemeOpen(false);
}}
selections={field.value}
variant={SelectVariant.single}
isOpen={adminUIThemeOpen}
label={t("adminTheme")}
labelIcon={t("adminThemeHelp")}
placeholderText={t("selectATheme")}
data-testid="select-admin-theme"
aria-label={t("selectAdminTheme")}
>
{themeTypes.admin
.filter((theme) => theme.name !== "base")
.map((theme, idx) => (
<SelectOption
selected={theme.name === field.value}
key={`admin-theme-${idx}`}
value={theme.name}
>
{t(theme.name)}
</SelectOption>
))}
</KeycloakSelect>
controller={{ defaultValue: "" }}
options={appendEmptyChoice(
themeTypes.admin.map((theme) => ({
key: theme.name,
value: theme.name,
})),
)}
/>
</FormGroup>
<FormGroup
label={t("emailTheme")}
fieldId="kc-email-theme"
labelIcon={
<HelpItem
helpText={t("emailThemeHelp")}
fieldLabelId="emailTheme"
/>
}
>
<Controller
<SelectControl
id="kc-email-theme"
name="emailTheme"
control={control}
defaultValue=""
render={({ field }) => (
<KeycloakSelect
toggleId="kc-email-theme"
onToggle={() => setEmailThemeOpen(!emailThemeOpen)}
onSelect={(value) => {
field.onChange(value as string);
setEmailThemeOpen(false);
}}
selections={field.value}
variant={SelectVariant.single}
isOpen={emailThemeOpen}
label={t("emailTheme")}
labelIcon={t("emailThemeHelp")}
placeholderText={t("selectATheme")}
data-testid="select-email-theme"
aria-label={t("selectEmailTheme")}
>
{themeTypes.email.map((theme, idx) => (
<SelectOption
selected={theme.name === field.value}
key={`email-theme-${idx}`}
value={theme.name}
>
{t(theme.name)}
</SelectOption>
))}
</KeycloakSelect>
controller={{ defaultValue: "" }}
options={appendEmptyChoice(
themeTypes.email.map((theme) => ({
key: theme.name,
value: theme.name,
})),
)}
/>
</FormGroup>
</FormProvider>
<ActionGroup>
<Button variant="primary" type="submit" data-testid="themes-tab-save">
{t("save")}

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import type { UserProfileGroup } from "@keycloak/keycloak-admin-client/lib/defs/userProfileMetadata";
import {
HelpItem,
@ -17,7 +18,6 @@ import {
TextContent,
TextInput,
} from "@patternfly/react-core";
import { GlobeRouteIcon } from "@patternfly/react-icons";
import { useEffect, useMemo, useState } from "react";
import {
FormProvider,
@ -27,7 +27,6 @@ import {
} from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Link, useNavigate, useParams } from "react-router-dom";
import { useAdminClient } from "../../admin-client";
import { FormAccess } from "../../components/form/FormAccess";
import { KeyValueInput } from "../../components/key-value-form/KeyValueInput";
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
@ -44,6 +43,8 @@ import {
AddTranslationsDialog,
TranslationsType,
} from "./attribute/AddTranslationsDialog";
import { GlobeRouteIcon } from "@patternfly/react-icons";
import { useAdminClient } from "../../admin-client";
function parseAnnotations(input: Record<string, unknown>): KeyValueType[] {
return Object.entries(input).reduce((p, [key, value]) => {
@ -77,11 +78,6 @@ type Translations = {
translations: TranslationForm[];
};
type TranslationsSets = {
displayHeader: Translations;
displayDescription: Translations;
};
const defaultValues: FormFields = {
annotations: [],
displayDescription: "",
@ -112,20 +108,21 @@ export default function AttributesGroupForm() {
const [addTranslationsModalOpen, toggleModal] = useToggle();
const regexPattern = /\$\{([^}]+)\}/;
const [type, setType] = useState<TranslationsType>();
const [translationsData, setTranslationsData] = useState<TranslationsSets>({
const [translationsData, setTranslationsData] = useState({
displayHeader: {
key: "",
translations: [],
translations: [] as TranslationForm[],
},
displayDescription: {
key: "",
translations: [],
translations: [] as TranslationForm[],
},
});
const matchingGroup = useMemo(
() => config?.groups?.find(({ name }) => name === params.name),
[config?.groups],
[config?.groups, params.name],
);
useEffect(() => {
@ -138,120 +135,98 @@ export default function AttributesGroupForm() {
: [];
form.reset({ ...defaultValues, ...matchingGroup, annotations });
}, [matchingGroup]);
}, [matchingGroup, form]);
useEffect(() => {
form.setValue(
"displayHeader",
matchingGroup
? matchingGroup.displayHeader!
: generatedAttributesGroupDisplayName,
matchingGroup?.displayHeader || generatedAttributesGroupDisplayName || "",
);
form.setValue(
"displayDescription",
matchingGroup
? matchingGroup.displayDescription!
: generatedAttributesGroupDisplayDescription,
matchingGroup?.displayDescription ||
generatedAttributesGroupDisplayDescription ||
"",
);
}, [
generatedAttributesGroupDisplayName,
generatedAttributesGroupDisplayDescription,
matchingGroup,
form,
]);
useFetch(
async () => {
const translationsToSaveDisplayHeader: Translations[] = [];
const translationsToSaveDisplayDescription: Translations[] = [];
const formData = form.getValues();
const translationsResults = await Promise.all(
combinedLocales.map(async (selectedLocale) => {
await Promise.all(
combinedLocales.map(async (locale: string) => {
try {
const translations =
await adminClient.realms.getRealmLocalizationTexts({
realm: realmName,
selectedLocale,
selectedLocale: locale,
});
const formattedDisplayHeaderKey = formData.displayHeader?.substring(
2,
formData.displayHeader.length - 1,
);
const formattedDisplayDescriptionKey =
formData.displayDescription?.substring(
2,
formData.displayDescription.length - 1,
);
return {
locale: selectedLocale,
headerTranslation: translations[formattedDisplayHeaderKey] ?? "",
descriptionTranslation:
translations[formattedDisplayDescriptionKey] ?? "",
const formData = form.getValues();
const extractKey = (value: string | undefined) => {
const match = value?.match(/\$\{(.*?)\}/);
return match ? match[1] : "";
};
const displayHeaderKey = extractKey(formData.displayHeader) || "";
const displayDescriptionKey =
extractKey(formData.displayDescription) || "";
const headerTranslation = translations[displayHeaderKey] || "";
const descriptionTranslation =
translations[displayDescriptionKey] || "";
if (headerTranslation) {
translationsToSaveDisplayHeader.push({
key: displayHeaderKey,
translations: [{ locale, value: headerTranslation }],
});
}
if (descriptionTranslation) {
translationsToSaveDisplayDescription.push({
key: displayDescriptionKey,
translations: [{ locale, value: descriptionTranslation }],
});
}
} catch (error) {
console.error(
`Error fetching translations for ${selectedLocale}:`,
error,
);
return null;
console.error(`Error fetching translations for ${locale}:`, error);
}
}),
);
translationsResults.forEach((translationsResult) => {
if (translationsResult) {
const { locale, headerTranslation, descriptionTranslation } =
translationsResult;
translationsToSaveDisplayHeader.push({
key: formData.displayHeader?.substring(
2,
formData.displayHeader.length - 1,
),
translations: [
{
locale,
value: headerTranslation,
},
],
});
translationsToSaveDisplayDescription.push({
key: formData.displayDescription?.substring(
2,
formData.displayDescription.length - 1,
),
translations: [
{
locale,
value: descriptionTranslation,
},
],
});
}
});
return {
translationsToSaveDisplayHeader,
translationsToSaveDisplayDescription,
};
},
(data) => {
setTranslationsData({
const translationsDataNew = {
displayHeader: {
key: data.translationsToSaveDisplayHeader[0].key,
translations: data.translationsToSaveDisplayHeader.flatMap(
(translationData) => translationData.translations,
key:
translationsToSaveDisplayHeader.length > 0
? translationsToSaveDisplayHeader[0].key
: "",
translations: translationsToSaveDisplayHeader.flatMap(
(data) => data.translations,
),
},
displayDescription: {
key: data.translationsToSaveDisplayDescription[0].key,
translations: data.translationsToSaveDisplayDescription.flatMap(
(translationData) => translationData.translations,
key:
translationsToSaveDisplayDescription.length > 0
? translationsToSaveDisplayDescription[0].key
: "",
translations: translationsToSaveDisplayDescription.flatMap(
(data) => data.translations,
),
},
});
};
setTranslationsData(translationsDataNew);
},
[combinedLocales],
() => {},
[combinedLocales, realmName, form],
);
const saveTranslations = async () => {
@ -278,10 +253,11 @@ export default function AttributesGroupForm() {
try {
if (
translationsData.displayHeader &&
translationsData &&
translationsData.displayHeader.translations.length > 0
) {
for (const translation of translationsData.displayHeader.translations) {
if (translation.locale && translation.value) {
await addLocalization(
translationsData.displayHeader.key,
translation.locale,
@ -289,13 +265,15 @@ export default function AttributesGroupForm() {
);
}
}
}
if (
translationsData.displayDescription &&
translationsData &&
translationsData.displayDescription.translations.length > 0
) {
for (const translation of translationsData.displayDescription
.translations) {
if (translation.locale && translation.value) {
await addLocalization(
translationsData.displayDescription.key,
translation.locale,
@ -303,6 +281,7 @@ export default function AttributesGroupForm() {
);
}
}
}
} catch (error) {
console.error(`Error while processing translations: ${error}`);
}
@ -331,7 +310,6 @@ export default function AttributesGroupForm() {
translationsData.displayHeader.translations.some(
(translation) => translation.value.trim() !== "",
);
const hasNonEmptyDisplayDescriptionTranslations =
translationsData.displayDescription.translations.some(
(translation) => translation.value.trim() !== "",

View file

@ -142,10 +142,13 @@ export const AddTranslationsDialog = ({
useEffect(() => {
combinedLocales.forEach((locale, rowIndex) => {
setValue(`translations.${rowIndex}.locale`, locale);
const translationExists =
translations.translations[rowIndex] !== undefined;
setValue(
`translations.${rowIndex}.value`,
translations.translations.length > 0
? translations.translations[rowIndex].value
translationExists
? translations.translations[rowIndex]?.value
: defaultTranslations[locale] || "",
);
});

View file

@ -3,6 +3,8 @@ import { useMatches } from "react-router-dom";
import { ForbiddenSection } from "../ForbiddenSection";
import { useAccess } from "../context/access/Access";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
function hasProp<K extends PropertyKey>(
data: object,
@ -14,6 +16,7 @@ function hasProp<K extends PropertyKey>(
export const AuthWall = ({ children }: any) => {
const matches = useMatches();
const { hasAccess } = useAccess();
const { whoAmI } = useWhoAmI();
const permissionNeeded = matches.flatMap(({ handle }) => {
if (
@ -31,9 +34,13 @@ export const AuthWall = ({ children }: any) => {
return [handle.access] as AccessType[];
});
return hasAccess(...permissionNeeded) ? (
children
) : (
<ForbiddenSection permissionNeeded={permissionNeeded} />
);
if (whoAmI.isEmpty()) {
return <KeycloakSpinner />;
}
if (!hasAccess(...permissionNeeded)) {
return <ForbiddenSection permissionNeeded={permissionNeeded} />;
}
return children;
};

View file

@ -45,14 +45,14 @@
"url-template": "^3.1.1"
},
"devDependencies": {
"@faker-js/faker": "^9.0.3",
"@faker-js/faker": "^9.2.0",
"@types/chai": "^5.0.1",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.9",
"@types/node": "^22.8.2",
"@types/node": "^22.9.0",
"chai": "^5.1.2",
"lodash-es": "^4.17.21",
"mocha": "^10.7.3",
"mocha": "^10.8.2",
"ts-node": "^10.9.2"
},
"author": {

View file

@ -83,6 +83,7 @@ export default interface RealmRepresentation {
offlineSessionMaxLifespan?: number;
offlineSessionMaxLifespanEnabled?: boolean;
organizationsEnabled?: boolean;
verifiableCredentialsEnabled?: boolean;
otpPolicyAlgorithm?: string;
otpPolicyDigits?: number;
otpPolicyInitialCounter?: number;

View file

@ -6,6 +6,14 @@ export class Cache extends Resource<{ realm?: string }> {
method: "POST",
path: "/clear-user-cache",
});
public clearKeysCache = this.makeRequest<{}, void>({
method: "POST",
path: "/clear-keys-cache",
});
public clearRealmCache = this.makeRequest<{}, void>({
method: "POST",
path: "/clear-realm-cache",
});
constructor(client: KeycloakAdminClient) {
super(client, {

View file

@ -15,7 +15,7 @@ describe("Attack Detection", () => {
kcAdminClient = new KeycloakAdminClient();
await kcAdminClient.auth(credentials);
const username = faker.internet.userName();
const username = faker.internet.username();
currentUser = await kcAdminClient.users.create({
username,
});

View file

@ -16,7 +16,7 @@ describe("Authentication management", () => {
before(async () => {
kcAdminClient = new KeycloakAdminClient();
await kcAdminClient.auth(credentials);
const realmName = faker.internet.userName().toLowerCase();
const realmName = faker.internet.username().toLowerCase();
await kcAdminClient.realms.create({
id: realmName,
realm: realmName,

View file

@ -27,7 +27,7 @@ describe("Clients", () => {
// create client and also test it
// NOTICE: to be clear, clientId stands for the property `clientId` of client
// clientUniqueId stands for property `id` of client
const clientId = faker.internet.userName();
const clientId = faker.internet.username();
const createdClient = await kcAdminClient.clients.create({
clientId,
});
@ -82,7 +82,7 @@ describe("Clients", () => {
it("delete single client", async () => {
// create another one for delete test
const clientId = faker.internet.userName();
const clientId = faker.internet.username();
const { id } = await kcAdminClient.clients.create({
clientId,
});
@ -103,7 +103,7 @@ describe("Clients", () => {
*/
describe("client roles", () => {
before(async () => {
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
// create a client role
const { roleName: createdRoleName } =
await kcAdminClient.clients.createRole({
@ -172,7 +172,7 @@ describe("Clients", () => {
});
it("delete a client role", async () => {
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
// create a client role
await kcAdminClient.clients.createRole({
id: currentClient.id,
@ -762,7 +762,7 @@ describe("Clients", () => {
it("get JSON with payload of examples", async () => {
const { id: clientUniqueId } = currentClient;
const username = faker.internet.userName();
const username = faker.internet.username();
const user = await kcAdminClient.users.create({
username,
});
@ -1019,7 +1019,7 @@ describe("Clients", () => {
});
before("create test user", async () => {
const username = faker.internet.userName();
const username = faker.internet.username();
user = await kcAdminClient.users.create({
username,
});

View file

@ -16,7 +16,7 @@ describe("User federation using component api", () => {
await kcAdminClient.auth(credentials);
// create user fed
const name = faker.internet.userName();
const name = faker.internet.username();
const component = await kcAdminClient.components.create({
name,
parentId: "master",

View file

@ -14,7 +14,7 @@ describe("Realms", () => {
kcAdminClient = new KeycloakAdminClient();
await kcAdminClient.auth(credentials);
const realmId = faker.internet.userName();
const realmId = faker.internet.username();
const realm = await kcAdminClient.realms.create({
id: realmId,
realm: realmId,
@ -28,7 +28,7 @@ describe("Realms", () => {
});
it("add a user to another realm", async () => {
const username = faker.internet.userName().toLowerCase();
const username = faker.internet.username().toLowerCase();
const user = await kcAdminClient.users.create({
realm: currentRealmId,
username,

View file

@ -21,7 +21,7 @@ describe("Group user integration", () => {
let currentPolicy: PolicyRepresentation;
before(async () => {
const groupName = faker.internet.userName();
const groupName = faker.internet.username();
kcAdminClient = new KeycloakAdminClient();
await kcAdminClient.auth(credentials);
// create group
@ -31,7 +31,7 @@ describe("Group user integration", () => {
currentGroup = (await kcAdminClient.groups.findOne({ id: group.id }))!;
// create user
const username = faker.internet.userName();
const username = faker.internet.username();
const user = await kcAdminClient.users.create({
username,
email: "test@keycloak.org",

View file

@ -100,7 +100,7 @@ describe("Groups", () => {
describe("role-mappings", () => {
before(async () => {
// create new role
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
const { roleName: createdRoleName } = await kcAdminClient.roles.create({
name: roleName,
});
@ -189,7 +189,7 @@ describe("Groups", () => {
describe("client role-mappings", () => {
before(async () => {
// create new client
const clientId = faker.internet.userName();
const clientId = faker.internet.username();
await kcAdminClient.clients.create({
clientId,
});
@ -199,7 +199,7 @@ describe("Groups", () => {
currentClient = clients[0];
// create new client role
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
await kcAdminClient.clients.createRole({
id: currentClient.id,
name: roleName,
@ -265,7 +265,7 @@ describe("Groups", () => {
});
it("del client role-mappings from group", async () => {
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
await kcAdminClient.clients.createRole({
id: currentClient.id,
name: roleName,

View file

@ -15,7 +15,7 @@ describe("Identity providers", () => {
await kcAdminClient.auth(credentials);
// create idp
const alias = faker.internet.userName();
const alias = faker.internet.username();
const idp = await kcAdminClient.identityProviders.create({
alias,
providerId: "saml",

View file

@ -10,8 +10,8 @@ import { credentials } from "./constants.js";
const expect = chai.expect;
const createRealm = async (kcAdminClient: KeycloakAdminClient) => {
const realmId = faker.internet.userName().toLowerCase();
const realmName = faker.internet.userName().toLowerCase();
const realmId = faker.internet.username().toLowerCase();
const realmName = faker.internet.username().toLowerCase();
const realm = await kcAdminClient.realms.create({
id: realmId,
realm: realmName,
@ -48,8 +48,8 @@ describe("Realms", () => {
});
it("create realm", async () => {
const realmId = faker.internet.userName().toLowerCase();
const realmName = faker.internet.userName().toLowerCase();
const realmId = faker.internet.username().toLowerCase();
const realmName = faker.internet.username().toLowerCase();
const realm = await kcAdminClient.realms.create({
id: realmId,
realm: realmName,

View file

@ -15,7 +15,7 @@ describe("Users federation provider", () => {
kcAdminClient = new KeycloakAdminClient();
await kcAdminClient.auth(credentials);
const name = faker.internet.userName();
const name = faker.internet.username();
currentUserFed = await kcAdminClient.components.create({
name,
parentId: "master",

View file

@ -34,7 +34,7 @@ describe("Users", () => {
});
// initialize user
const username = faker.internet.userName();
const username = faker.internet.username();
const user = await kcAdminClient.users.create({
username,
email: "test@keycloak.org",
@ -373,7 +373,7 @@ describe("Users", () => {
describe("role-mappings", () => {
before(async () => {
// create new role
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
await kcAdminClient.roles.create({
name: roleName,
});
@ -460,7 +460,7 @@ describe("Users", () => {
describe("client role-mappings", () => {
before(async () => {
// create new client
const clientId = faker.internet.userName();
const clientId = faker.internet.username();
await kcAdminClient.clients.create({
clientId,
});
@ -470,7 +470,7 @@ describe("Users", () => {
currentClient = clients[0];
// create new client role
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
await kcAdminClient.clients.createRole({
id: currentClient.id,
name: roleName,
@ -536,7 +536,7 @@ describe("Users", () => {
});
it("del client role-mappings from user", async () => {
const roleName = faker.internet.userName();
const roleName = faker.internet.username();
await kcAdminClient.clients.createRole({
id: currentClient.id,
name: roleName,
@ -575,7 +575,7 @@ describe("Users", () => {
await kcAdminClient.auth(credentials);
// create client
const clientId = faker.internet.userName();
const clientId = faker.internet.username();
await kcAdminClient.clients.create({
clientId,
consentRequired: true,

View file

@ -1299,30 +1299,33 @@ function Keycloak (config) {
return;
}
const logoutUrl = kc.createLogoutUrl(options);
const response = await fetch(logoutUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
// Create form to send POST request.
const form = document.createElement("form");
form.setAttribute("method", "POST");
form.setAttribute("action", kc.createLogoutUrl(options));
form.style.display = "none";
// Add data to form as hidden input fields.
const data = {
id_token_hint: kc.idToken,
client_id: kc.clientId,
post_logout_redirect_uri: adapter.redirectUri(options, false)
})
});
};
if (response.redirected) {
window.location.href = response.url;
return;
for (const [name, value] of Object.entries(data)) {
const input = document.createElement("input");
input.setAttribute("type", "hidden");
input.setAttribute("name", name);
input.setAttribute("value", value);
form.appendChild(input);
}
if (response.ok) {
window.location.reload();
return;
}
throw new Error("Logout failed, request returned an error code.");
// Append form to page and submit it to perform logout and redirect.
document.body.appendChild(form);
form.submit();
},
register: async function(options) {

View file

@ -48,7 +48,7 @@
"@patternfly/react-core": "^5.4.8",
"@patternfly/react-icons": "^5.4.2",
"@patternfly/react-styles": "^5.4.1",
"@patternfly/react-table": "^5.4.8",
"@patternfly/react-table": "^5.4.9",
"i18next": "^23.16.4",
"keycloak-js": "workspace:*",
"lodash-es": "^4.17.21",

View file

@ -5,9 +5,9 @@ import {
Page,
Text,
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { getNetworkErrorDescription } from "../utils/errors";
type ErrorPageProps = {
error?: unknown;
@ -16,7 +16,10 @@ type ErrorPageProps = {
export const ErrorPage = (props: ErrorPageProps) => {
const { t } = useTranslation();
const error = props.error;
const errorMessage = getErrorMessage(error);
const errorMessage =
getErrorMessage(error) ||
getNetworkErrorDescription(error)?.replace(/\+/g, " ");
console.error(error);
function onRetry() {
location.href = location.origin + location.pathname;
@ -26,7 +29,7 @@ export const ErrorPage = (props: ErrorPageProps) => {
<Page>
<Modal
variant={ModalVariant.small}
title={t("somethingWentWrong")}
title={errorMessage ? "" : t("somethingWentWrong")}
titleIconVariant="danger"
showClose={false}
isOpen
@ -37,9 +40,10 @@ export const ErrorPage = (props: ErrorPageProps) => {
]}
>
<TextContent>
{errorMessage ? (
<Text>{t(errorMessage)}</Text>
) : (
<Text>{t("somethingWentWrongDescription")}</Text>
{errorMessage && (
<Text component={TextVariants.small}>{errorMessage}</Text>
)}
</TextContent>
</Modal>

View file

@ -79,8 +79,14 @@ export const KeycloakProvider = <T extends BaseEnvironment>({
calledOnce.current = true;
}, [keycloak]);
if (error) {
return <ErrorPage error={error} />;
const searchParams = new URLSearchParams(window.location.search);
if (error || searchParams.get("error_description")) {
return (
<ErrorPage
error={error ? error : searchParams.get("error_description")}
/>
);
}
if (!init) {

View file

@ -94,7 +94,7 @@ export const SingleSelectControl = <
}}
isOpen={open}
>
<SelectList>
<SelectList data-testid={`select-${name}`}>
{options.map((option) => (
<SelectOption key={key(option)} value={key(option)}>
{isString(option) ? option : option.value}

View file

@ -82,7 +82,7 @@ export const SelectComponent = (props: UserProfileFieldProps) => {
}}
selections={
isMultiValue && Array.isArray(field.value)
? field.value
? field.value.map((option) => fetchLabel(option))
: fetchLabel(field.value)
}
variant={

View file

@ -1821,8 +1821,25 @@ public class RealmAdapter implements CachedRealmModel {
updated.setOrganizationsEnabled(organizationsEnabled);
}
@Override
public boolean isVerifiableCredentialsEnabled() {
if (isUpdated()) return featureVerifiableCredentialsEnabled(updated.isVerifiableCredentialsEnabled());
return featureVerifiableCredentialsEnabled(cached.isVerifiableCredentialsEnabled());
}
@Override
public void setVerifiableCredentialsEnabled(boolean verifiableCredentialsEnabled) {
getDelegateForUpdate();
updated.setVerifiableCredentialsEnabled(verifiableCredentialsEnabled);
}
private boolean featureAwareIsOrganizationsEnabled(boolean isOrganizationsEnabled) {
if (!Profile.isFeatureEnabled(Profile.Feature.ORGANIZATION)) return false;
return isOrganizationsEnabled;
}
private boolean featureVerifiableCredentialsEnabled(boolean isVerifiableCredentialsEnabled) {
if (!Profile.isFeatureEnabled(Profile.Feature.OID4VC_VCI)) return false;
return isVerifiableCredentialsEnabled;
}
}

View file

@ -75,6 +75,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected boolean identityFederationEnabled;
protected boolean editUsernameAllowed;
protected boolean organizationsEnabled;
protected boolean verifiableCredentialsEnabled;
//--- brute force settings
protected boolean bruteForceProtected;
protected boolean permanentLockout;
@ -191,6 +192,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
resetPasswordAllowed = model.isResetPasswordAllowed();
editUsernameAllowed = model.isEditUsernameAllowed();
organizationsEnabled = model.isOrganizationsEnabled();
verifiableCredentialsEnabled = model.isVerifiableCredentialsEnabled();
//--- brute force settings
bruteForceProtected = model.isBruteForceProtected();
permanentLockout = model.isPermanentLockout();
@ -431,6 +433,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return organizationsEnabled;
}
public boolean isVerifiableCredentialsEnabled() {
return verifiableCredentialsEnabled;
}
public String getDefaultSignatureAlgorithm() {
return defaultSignatureAlgorithm;
}

View file

@ -1199,6 +1199,16 @@ public class RealmAdapter implements StorageProviderRealmModel, JpaModel<RealmEn
setAttribute(RealmAttributes.ORGANIZATIONS_ENABLED, organizationsEnabled);
}
@Override
public boolean isVerifiableCredentialsEnabled() {
return getAttribute(RealmAttributes.VERIFIABLE_CREDENTIALS_ENABLED, Boolean.FALSE);
}
@Override
public void setVerifiableCredentialsEnabled(boolean verifiableCredentialsEnabled) {
setAttribute(RealmAttributes.VERIFIABLE_CREDENTIALS_ENABLED, verifiableCredentialsEnabled);
}
@Override
public ClientModel getMasterAdminClient() {
String masterAdminClientId = realm.getMasterAdminClient();

View file

@ -56,5 +56,7 @@ public interface RealmAttributes {
String FIRST_BROKER_LOGIN_FLOW_ID = "firstBrokerLoginFlowId";
String VERIFIABLE_CREDENTIALS_ENABLED = "verifiableCredentialsEnabled";
String ORGANIZATIONS_ENABLED = "organizationsEnabled";
}

Some files were not shown because too many files have changed in this diff Show more