diff --git a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java index 6276e36396be..6040cf8d376f 100755 --- a/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/account/UserRepresentation.java @@ -17,128 +17,11 @@ package org.keycloak.representations.account; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.keycloak.json.StringListMapDeserializer; -import org.keycloak.representations.idm.UserProfileMetadata; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import org.keycloak.representations.idm.AbstractUserRepresentation; /** * @author Stian Thorgersen */ -public class UserRepresentation { - - private String id; - private String username; - private String firstName; - private String lastName; - private String email; - private boolean emailVerified; - private UserProfileMetadata userProfileMetadata; - - @JsonDeserialize(using = StringListMapDeserializer.class) - private Map> attributes; - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public boolean isEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(boolean emailVerified) { - this.emailVerified = emailVerified; - } - - public Map> getAttributes() { - return attributes; - } - - public void setAttributes(Map> attributes) { - this.attributes = attributes; - } - - public void singleAttribute(String name, String value) { - if (this.attributes == null) this.attributes=new HashMap<>(); - attributes.put(name, (value == null ? new ArrayList() : Arrays.asList(value))); - } - - public String firstAttribute(String key) { - return this.attributes == null ? null : this.attributes.containsKey(key) ? this.attributes.get(key).get(0) : null; - } - - public Map> toAttributes() { - Map> attrs = new HashMap<>(); - - if (getAttributes() != null) attrs.putAll(getAttributes()); - - if (getUsername() != null) - attrs.put("username", Collections.singletonList(getUsername())); - else - attrs.remove("username"); - - if (getEmail() != null) - attrs.put("email", Collections.singletonList(getEmail())); - else - attrs.remove("email"); - - if (getLastName() != null) - attrs.put("lastName", Collections.singletonList(getLastName())); - - if (getFirstName() != null) - attrs.put("firstName", Collections.singletonList(getFirstName())); - - - return attrs; - } - - public UserProfileMetadata getUserProfileMetadata() { - return userProfileMetadata; - } +public class UserRepresentation extends AbstractUserRepresentation { - public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { - this.userProfileMetadata = userProfileMetadata; - } } diff --git a/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java new file mode 100644 index 000000000000..c4fe9cb7a761 --- /dev/null +++ b/core/src/main/java/org/keycloak/representations/idm/AbstractUserRepresentation.java @@ -0,0 +1,157 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.representations.idm; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.keycloak.json.StringListMapDeserializer; + +public abstract class AbstractUserRepresentation { + + public static String USERNAME = "username"; + public static String FIRST_NAME = "firstName"; + public static String LAST_NAME = "lastName"; + public static String EMAIL = "email"; + public static String LOCALE = "locale"; + + protected String id; + protected String username; + protected String firstName; + protected String lastName; + protected String email; + protected Boolean emailVerified; + @JsonDeserialize(using = StringListMapDeserializer.class) + protected Map> attributes; + private UserProfileMetadata userProfileMetadata; + + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Boolean isEmailVerified() { + return emailVerified; + } + + public void setEmailVerified(Boolean emailVerified) { + this.emailVerified = emailVerified; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns all the attributes set to this user except the root attributes. + * + * @return the user attributes. + */ + public Map> getAttributes() { + return attributes; + } + + /** + * Returns all the user attributes including the root attributes. + * + * @return all the user attributes. + */ + @JsonIgnore + public Map> getRawAttributes() { + Map> attrs = new HashMap<>(Optional.ofNullable(attributes).orElse(new HashMap<>())); + + if (username != null) + attrs.put(USERNAME, Collections.singletonList(getUsername())); + else + attrs.remove(USERNAME); + + if (email != null) + attrs.put(EMAIL, Collections.singletonList(getEmail())); + else + attrs.remove(EMAIL); + + if (lastName != null) + attrs.put(LAST_NAME, Collections.singletonList(getLastName())); + + if (firstName != null) + attrs.put(FIRST_NAME, Collections.singletonList(getFirstName())); + + return attrs; + } + + public void setAttributes(Map> attributes) { + this.attributes = attributes; + } + + @SuppressWarnings("unchecked") + public R singleAttribute(String name, String value) { + if (this.attributes == null) this.attributes=new HashMap<>(); + attributes.put(name, (value == null ? Collections.emptyList() : Arrays.asList(value))); + return (R) this; + } + + public String firstAttribute(String key) { + return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0); + } + + public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { + this.userProfileMetadata = userProfileMetadata; + } + + public UserProfileMetadata getUserProfileMetadata() { + return userProfileMetadata; + } +} diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 6871796e35a1..8ab1110bde9f 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -17,14 +17,7 @@ package org.keycloak.representations.idm; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import org.keycloak.json.StringListMapDeserializer; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.ArrayList; import java.util.Map; import java.util.Set; @@ -32,24 +25,16 @@ * @author Bill Burke * @version $Revision: 1 $ */ -public class UserRepresentation { +public class UserRepresentation extends AbstractUserRepresentation{ protected String self; // link - protected String id; protected String origin; protected Long createdTimestamp; - protected String username; protected Boolean enabled; protected Boolean totp; - protected Boolean emailVerified; - protected String firstName; - protected String lastName; - protected String email; protected String federationLink; protected String serviceAccountClientId; // For rep, it points to clientId (not DB ID) - @JsonDeserialize(using = StringListMapDeserializer.class) - protected Map> attributes; protected List credentials; protected Set disableableCredentialTypes; protected List requiredActions; @@ -66,7 +51,6 @@ public class UserRepresentation { protected List groups; private Map access; - private UserProfileMetadata userProfileMetadata; public String getSelf() { return self; @@ -76,14 +60,6 @@ public void setSelf(String self) { this.self = self; } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - public Long getCreatedTimestamp() { return createdTimestamp; } @@ -92,38 +68,6 @@ public void setCreatedTimestamp(Long createdTimestamp) { this.createdTimestamp = createdTimestamp; } - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - public Boolean isEnabled() { return enabled; } @@ -142,32 +86,6 @@ public void setTotp(Boolean totp) { this.totp = totp; } - public Boolean isEmailVerified() { - return emailVerified; - } - - public void setEmailVerified(Boolean emailVerified) { - this.emailVerified = emailVerified; - } - - public Map> getAttributes() { - return attributes; - } - - public void setAttributes(Map> attributes) { - this.attributes = attributes; - } - - public UserRepresentation singleAttribute(String name, String value) { - if (this.attributes == null) this.attributes=new HashMap<>(); - attributes.put(name, (value == null ? new ArrayList() : Arrays.asList(value))); - return this; - } - - public String firstAttribute(String key) { - return this.attributes == null ? null : this.attributes.get(key) == null ? null : this.attributes.get(key).isEmpty()? null : this.attributes.get(key).get(0); - } - public List getCredentials() { return credentials; } @@ -289,36 +207,4 @@ public Map getAccess() { public void setAccess(Map access) { this.access = access; } - - public Map> toAttributes() { - Map> attrs = new HashMap<>(); - - if (getAttributes() != null) attrs.putAll(getAttributes()); - - if (getUsername() != null) - attrs.put("username", Collections.singletonList(getUsername())); - else - attrs.remove("username"); - - if (getEmail() != null) - attrs.put("email", Collections.singletonList(getEmail())); - else - attrs.remove("email"); - - if (getLastName() != null) - attrs.put("lastName", Collections.singletonList(getLastName())); - - if (getFirstName() != null) - attrs.put("firstName", Collections.singletonList(getFirstName())); - - return attrs; - } - - public void setUserProfileMetadata(UserProfileMetadata userProfileMetadata) { - this.userProfileMetadata = userProfileMetadata; - } - - public UserProfileMetadata getUserProfileMetadata() { - return userProfileMetadata; - } } diff --git a/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java b/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java index 86a9af8f758c..0992e31d4c8c 100644 --- a/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java +++ b/core/src/main/java/org/keycloak/representations/userprofile/config/UPConfig.java @@ -32,8 +32,20 @@ public class UPConfig { public enum UnmanagedAttributePolicy { + + /** + * Unmanaged attributes are enabled and available from any context. + */ ENABLED, + + /** + * Unmanaged attributes are only available as read-only and only through the management interfaces. + */ ADMIN_VIEW, + + /** + * Unmanaged attributes are only available as read-write and only through the management interfaces. + */ ADMIN_EDIT } diff --git a/docs/documentation/release_notes/topics/24_0_0.adoc b/docs/documentation/release_notes/topics/24_0_0.adoc index cc5a3d8db30b..b32f6d32a1cb 100644 --- a/docs/documentation/release_notes/topics/24_0_0.adoc +++ b/docs/documentation/release_notes/topics/24_0_0.adoc @@ -34,3 +34,18 @@ As a result, you can use the CR to configure build time options also when a cust It is now possible to separately enable parsing of either `Forwarded` or `X-Forwarded-*` headers via the new `--proxy-headers` option. For details consult the https://www.keycloak.org/server/reverseproxy[Reverse Proxy Guide]. The original `--proxy` option is now deprecated and will be removed in a future release. For migration instructions consult the link:{upgradingguide_link}[{upgradingguide_name}]. + += Breaking changes to the User Profile SPI + +In this release, there are changes to the User Profile SPI that might impact existing implementations based on this SPI. For more details, check the +link:{upgradingguide_link}[{upgradingguide_name}]. + += Changes to the user representation in both Admin API and Account contexts + +In this release, we are encapsulating the root user attributes (such as `username`, `email`, `firstName`, `lastName`, and `locale`) by moving them to a base/abstract class in order to align how these attributes +are marshalled and unmarshalled when using both Admin and Account REST APIs. + +This strategy provides consistency in how attributes are managed by clients and makes sure they conform to the user profile +configuration set to a realm. + +For more details, see link:{upgradingguide_link}[{upgradingguide_name}]. \ No newline at end of file diff --git a/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc b/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc index 72cf564d92cf..97b98e07da5e 100644 --- a/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc +++ b/docs/documentation/upgrading/topics/keycloak/changes-24_0_0.adoc @@ -40,4 +40,29 @@ The `--proxy` option has been deprecated and will be removed in a future release |=== NOTE: For hardened security, the `--proxy-headers` option does not allow selecting both `forwarded` and `xforwarded` values at the same time (as it was -the case before for `--proxy edge` and `--proxy reencrypt`). \ No newline at end of file +the case before for `--proxy edge` and `--proxy reencrypt`). + += Breaking changes to the User Profile SPI + +If you are using the User Profile SPI in your extension, you might be impacted by the API changes introduced in this release. + +The `org.keycloak.userprofile.Attributes` interface includes the following changes: + +* Method `getValues` was renamed to `get` to make it more aligned with the same operation from a regular Java `Map` +* Method `isRootAttribute` was moved to the utility class `org.keycloak.userprofile.UserProfileUtil.isRootAttribute` +* Method `getFirstValue` was renamed to `getFirst` to make it less verbose +* Method `getReadable(boolean)` was removed and now all attributes (including root attributes) are returned whenever they have read rights. + += Changes to the user representation in both Admin API and Account contexts + +Both `org.keycloak.representations.idm.UserRepresentation` and `org.keycloak.representations.account.UserRepresentation` representation classes have changed +so that the root user attributes (such as `username`, `email`, `firstName`, `lastName`, and `locale`) have a consistent representation when fetching or sending +the representation payload to the Admin and Account APIS, respectively. + +The `username`, `email`, `firstName`, `lastName`, and `locale` attributes were moved to a new `org.keycloak.representations.idm.AbstractUserRepresentation` base class. + +Also the `getAttributes` method is targeted for representing only custom attributes, so you should not expect any root attribute in the map returned by this method. This method is +mainly targeted for clients when updating or fetching any custom attribute for a give user. + +In order to resolve all the attributes including the root attributes, a new `getRawAttributes` method was added so that the resulting map also includes the root attributes. However, +this method is not available from the representation payload and it is targeted to be used by the server when managing user profiles. \ No newline at end of file diff --git a/js/apps/account-ui/package.json b/js/apps/account-ui/package.json index 1a3f9d74e5dc..94ae7bd3e07b 100644 --- a/js/apps/account-ui/package.json +++ b/js/apps/account-ui/package.json @@ -13,7 +13,7 @@ "@patternfly/react-core": "^4.278.0", "@patternfly/react-icons": "^4.93.7", "@patternfly/react-table": "^4.113.6", - "i18next": "^23.7.8", + "i18next": "^23.7.9", "i18next-http-backend": "^2.4.2", "keycloak-js": "workspace:*", "keycloak-masthead": "workspace:*", @@ -29,11 +29,11 @@ "@keycloak/keycloak-admin-client": "workspace:*", "@playwright/test": "^1.40.1", "@types/lodash-es": "^4.17.12", - "@types/react": "^18.2.43", + "@types/react": "^18.2.44", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react-swc": "^3.5.0", "lightningcss": "^1.22.1", - "vite": "^5.0.6", + "vite": "^5.0.8", "vite-plugin-checker": "^0.6.2" }, "wireit": { diff --git a/js/apps/account-ui/pom.xml b/js/apps/account-ui/pom.xml index 4c0e60a4326f..e2cbb8e115b3 100644 --- a/js/apps/account-ui/pom.xml +++ b/js/apps/account-ui/pom.xml @@ -116,6 +116,7 @@ "resourceUrl": "${resourceUrl}", "logo": "${properties.logo!""}", "logoUrl": "${properties.logoUrl!""}", + "locale": "${locale}", "features": { "isRegistrationEmailAsUsername": ${realm.registrationEmailAsUsername?c}, "isEditUserNameAllowed": ${realm.editUsernameAllowed?c}, diff --git a/js/apps/account-ui/src/environment.ts b/js/apps/account-ui/src/environment.ts index 8aaf77e27603..425e0c26d9a8 100644 --- a/js/apps/account-ui/src/environment.ts +++ b/js/apps/account-ui/src/environment.ts @@ -11,6 +11,8 @@ export type Environment = { logo: string; /** Indicates the url to be followed when Brand image is clicked */ logoUrl: string; + /** The locale of the user */ + locale: string; /** Feature flags */ features: { isRegistrationEmailAsUsername: boolean; @@ -36,6 +38,7 @@ const defaultEnvironment: Environment = { resourceUrl: "http://localhost:8080", logo: "/logo.svg", logoUrl: "/", + locale: "en", features: { isRegistrationEmailAsUsername: false, isEditUserNameAllowed: true, diff --git a/js/apps/account-ui/src/i18n.ts b/js/apps/account-ui/src/i18n.ts index e305f17f6425..b8c42565fa7b 100644 --- a/js/apps/account-ui/src/i18n.ts +++ b/js/apps/account-ui/src/i18n.ts @@ -1,4 +1,4 @@ -import { createInstance } from "i18next"; +import { LanguageDetectorModule, createInstance } from "i18next"; import HttpBackend from "i18next-http-backend"; import { initReactI18next } from "react-i18next"; @@ -14,6 +14,14 @@ type KeyValue = { key: string; value: string }; // that we can have a proper type-safe translation function. export type TFuncKey = any; +export const keycloakLanguageDetector: LanguageDetectorModule = { + type: "languageDetector", + + detect() { + return environment.locale; + }, +}; + export const i18n = createInstance({ fallbackLng: DEFAULT_LOCALE, interpolation: { @@ -35,4 +43,5 @@ export const i18n = createInstance({ }); i18n.use(HttpBackend); +i18n.use(keycloakLanguageDetector); i18n.use(initReactI18next); diff --git a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx index c7923a74c203..eec3b80b5597 100644 --- a/js/apps/account-ui/src/personal-info/PersonalInfo.tsx +++ b/js/apps/account-ui/src/personal-info/PersonalInfo.tsx @@ -29,7 +29,7 @@ import { } from "../api/representations"; import { Page } from "../components/page/Page"; import { environment } from "../environment"; -import { TFuncKey } from "../i18n"; +import { TFuncKey, i18n } from "../i18n"; import { usePromise } from "../utils/usePromise"; const PersonalInfo = () => { @@ -58,6 +58,12 @@ const PersonalInfo = () => { const onSubmit = async (user: UserRepresentation) => { try { await savePersonalInfo(user); + const locale = user.attributes?.["locale"]?.toString(); + i18n.changeLanguage(locale, (error) => { + if (error) { + console.warn("Error(s) loading locale", locale, error); + } + }); keycloak?.updateToken(); addAlert(t("accountUpdatedMessage")); } catch (error) { diff --git a/js/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts b/js/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts index 0cdbc73cdd64..6adbf2199ddf 100644 --- a/js/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/client_registration_policies.spec.ts @@ -29,10 +29,10 @@ describe("Client registration policies tab", () => { it("add anonymous client registration policy", () => { clientRegistrationPage - .createPolicy() + .createAnonymousPolicy() .selectRow("max-clients") .fillPolicyForm({ - name: "new policy", + name: "newAnonymPolicy1", }) .formUtils() .save(); @@ -41,32 +41,33 @@ describe("Client registration policies tab", () => { "New client policy created successfully", ); clientRegistrationPage.formUtils().cancel(); - listingPage.itemExist("new policy"); + listingPage.itemExist("newAnonymPolicy1"); }); it("edit anonymous client registration policy", () => { - listingPage.goToItemDetails("new policy"); + const policy = "newAnonymPolicy1"; + clientRegistrationPage.findAndSelectInAnonymousPoliciesTable(policy); cy.findByTestId("name").clear(); clientRegistrationPage .fillPolicyForm({ - name: "policy 2", + name: "policy2", }) .formUtils() .save(); masthead.checkNotificationMessage("Client policy updated successfully"); clientRegistrationPage.formUtils().cancel(); - listingPage.itemExist("policy 2"); + listingPage.itemExist("policy2"); }); it("delete anonymous client registration policy", () => { - listingPage.clickRowDetails("policy 2").clickDetailMenu("Delete"); - clientRegistrationPage.modalUtils().confirmModal(); + const policy = "policy2"; + listingPage.deleteItem(policy); + cy.findByTestId("confirm").click(); masthead.checkNotificationMessage( "Client registration policy deleted successfully", ); - listingPage.itemExist("policy 2", false); }); }); @@ -84,10 +85,10 @@ describe("Client registration policies tab", () => { it("add authenticated client registration policy", () => { clientRegistrationPage - .createPolicy() + .createAuthenticatedPolicy() .selectRow("scope") .fillPolicyForm({ - name: "new authenticated policy", + name: "newAuthPolicy1", }) .formUtils() .save(); @@ -96,32 +97,33 @@ describe("Client registration policies tab", () => { "New client policy created successfully", ); clientRegistrationPage.formUtils().cancel(); - listingPage.itemExist("new authenticated policy"); + listingPage.itemExist("newAuthPolicy1"); }); it("edit authenticated client registration policy", () => { - listingPage.goToItemDetails("new authenticated policy"); + const policy = "newAuthPolicy1"; + clientRegistrationPage.findAndSelectInAuthenticatedPoliciesTable(policy); cy.findByTestId("name").clear(); clientRegistrationPage .fillPolicyForm({ - name: "policy 3", + name: "policy3", }) .formUtils() .save(); masthead.checkNotificationMessage("Client policy updated successfully"); clientRegistrationPage.formUtils().cancel(); - listingPage.itemExist("policy 3"); + listingPage.itemExist("policy3"); }); it("delete authenticated client registration policy", () => { - listingPage.clickRowDetails("policy 3").clickDetailMenu("Delete"); - clientRegistrationPage.modalUtils().confirmModal(); + const policy = "policy3"; + listingPage.deleteItem(policy); + cy.findByTestId("confirm").click(); masthead.checkNotificationMessage( "Client registration policy deleted successfully", ); - listingPage.itemExist("policy 3", false); }); }); }); diff --git a/js/apps/admin-ui/cypress/e2e/partial_export_test.spec.ts b/js/apps/admin-ui/cypress/e2e/partial_export_test.spec.ts index 4fd912afb641..92ff4856a5cf 100644 --- a/js/apps/admin-ui/cypress/e2e/partial_export_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/partial_export_test.spec.ts @@ -45,7 +45,7 @@ describe("Partial realm export", () => { it("Exports the realm", () => { modal.includeGroupsAndRolesSwitch().click({ force: true }); - modal.includeGroupsAndRolesSwitch().click({ force: true }); + modal.includeClientsSwitch().click({ force: true }); modal.exportButton().click(); cy.readFile( Cypress.config("downloadsFolder") + "/realm-export.json", diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_client_profiles_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_client_profiles_test.spec.ts index 28dc764e374b..63f8811a5e66 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_client_profiles_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_client_profiles_test.spec.ts @@ -58,6 +58,17 @@ describe("Realm settings client profiles tab tests", () => { realmSettingsPage.searchClientProfile(profileName); }); + it("Should search non-existent client profile", () => { + realmSettingsPage.searchNonExistingClientProfile("nonExistentProfile"); + cy.findByTestId("empty-state").should("be.visible"); + }); + + it("Should navigate to client profile", () => { + realmSettingsPage.searchClientProfile(profileName); + realmSettingsPage.goToClientProfileByNameLink(profileName); + cy.findByTestId("view-header").should("have.text", profileName); + }); + it("Check navigating between Form View and JSON editor", () => { realmSettingsPage.shouldNavigateBetweenFormAndJSONView(); }); diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_events_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_events_test.spec.ts index 274a8e0b0e3e..25de676ba478 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_events_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_events_test.spec.ts @@ -269,33 +269,6 @@ describe("Realm settings events tab tests", () => { cy.findByTestId("localization-tab-save").click(); }); - it("Realm header settings", () => { - sidebarPage.goToRealmSettings(); - cy.findByTestId("rs-security-defenses-tab").click(); - cy.findByTestId("headers-form-tab-save").should("be.disabled"); - cy.get("#xFrameOptions").clear().type("DENY"); - cy.findByTestId("headers-form-tab-save").should("be.enabled").click(); - - masthead.checkNotificationMessage("Realm successfully updated"); - }); - - it("Brute force detection", () => { - sidebarPage.goToRealmSettings(); - cy.findAllByTestId("rs-security-defenses-tab").click(); - cy.get("#pf-tab-20-bruteForce").click(); - - cy.findByTestId("brute-force-tab-save").should("be.disabled"); - - cy.get("#bruteForceProtected").click({ force: true }); - cy.findByTestId("waitIncrementSeconds").type("1"); - cy.findByTestId("maxFailureWaitSeconds").type("1"); - cy.findByTestId("maxDeltaTimeSeconds").type("1"); - cy.findByTestId("minimumQuickLoginWaitSeconds").type("1"); - - cy.findByTestId("brute-force-tab-save").should("be.enabled").click(); - masthead.checkNotificationMessage("Realm successfully updated"); - }); - it("add session data", () => { sidebarPage.goToRealmSettings(); diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_general_tab_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_general_tab_test.spec.ts index 8d221ea77737..9d148767aa75 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_general_tab_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_general_tab_test.spec.ts @@ -61,6 +61,13 @@ describe("Realm settings general tab tests", () => { masthead.checkNotificationMessage("Realm successfully updated"); }); + it("Fail to set Realm ID to empty", () => { + sidebarPage.goToRealmSettings(); + realmSettingsPage.clearRealmId(); + realmSettingsPage.saveGeneral(); + cy.get("#kc-realm-id-helper").should("have.text", "Required field"); + }); + it("Modify Display name", () => { sidebarPage.goToRealmSettings(); realmSettingsPage.fillDisplayName("display_name"); diff --git a/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts b/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts index 41ae7000a330..7f1e125bd976 100644 --- a/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts +++ b/js/apps/admin-ui/cypress/e2e/realm_settings_tabs_test.spec.ts @@ -131,6 +131,42 @@ describe("Realm settings tabs tests", () => { realmSettingsPage.saveThemes(); }); + describe("Go to security defenses tab", () => { + it("Realm header settings- update single input", () => { + sidebarPage.goToRealmSettings(); + realmSettingsPage.goToSecurityDefensesTab(); + cy.get("#xFrameOptions").clear().type("DENY"); + realmSettingsPage.saveSecurityDefensesHeaders(); + masthead.checkNotificationMessage("Realm successfully updated"); + }); + it("Realm header settings- update all inputs", () => { + sidebarPage.goToRealmSettings(); + realmSettingsPage.goToSecurityDefensesTab(); + cy.get("#xFrameOptions").clear().type("SAMEORIGIN"); + cy.get("#contentSecurityPolicy").clear().type("default-src 'self'"); + cy.get("#strictTransportSecurity").clear().type("max-age=31536000"); + cy.get("#xContentTypeOptions").clear().type("nosniff"); + cy.get("#xRobotsTag").clear().type("none"); + cy.get("#xXSSProtection").clear().type("1; mode=block"); + cy.get("#strictTransportSecurity").clear().type("max-age=31537000"); + cy.get("#referrerPolicy").clear().type("referrer"); + realmSettingsPage.saveSecurityDefensesHeaders(); + masthead.checkNotificationMessage("Realm successfully updated"); + }); + it("Brute force detection- update values", () => { + sidebarPage.goToRealmSettings(); + realmSettingsPage.goToSecurityDefensesTab(); + realmSettingsPage.goToSecurityDefensesBruteForceTab(); + cy.get("#bruteForceProtected").click({ force: true }); + cy.findByTestId("waitIncrementSeconds").type("1"); + cy.findByTestId("maxFailureWaitSeconds").type("1"); + cy.findByTestId("maxDeltaTimeSeconds").type("1"); + cy.findByTestId("minimumQuickLoginWaitSeconds").type("1"); + realmSettingsPage.saveSecurityDefensesBruteForce(); + masthead.checkNotificationMessage("Realm successfully updated"); + }); + }); + describe("Accessibility tests for realm settings", () => { beforeEach(() => { loginPage.logIn(); diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts index bd7db4fa0467..62c1d5206319 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/ClientRegistrationPage.ts @@ -16,6 +16,30 @@ export class ClientRegistrationPage extends CommonPage { return this; } + createAnonymousPolicy() { + cy.findByTestId("createPolicy-anonymous").click(); + return this; + } + + createAuthenticatedPolicy() { + cy.findByTestId("createPolicy-authenticated").click(); + return this; + } + + findAndSelectInAnonymousPoliciesTable(policy: string) { + cy.findByTestId("clientRegistration-anonymous") + .find("tr") + .contains(policy) + .click(); + } + + findAndSelectInAuthenticatedPoliciesTable(policy: string) { + cy.findByTestId("clientRegistration-authenticated") + .find("tr") + .contains(policy) + .click(); + } + selectRow(name: string) { cy.findAllByTestId(name).click(); return this; diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts index cc7699382f01..c5e7e5257495 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/realm_settings/RealmSettingsPage.ts @@ -239,6 +239,12 @@ export default class RealmSettingsPage extends CommonPage { #publicKeyBtn = ".kc-keys-list > tbody > tr > td > .button-wrapper > button"; #realmSettingsEventsTab = new RealmSettingsEventsTab(); + #realmId = 'input[aria-label="Copyable input"]'; + #securityDefensesHeadersSaveBtn = "headers-form-tab-save"; + #securityDefensesBruteForceSaveBtn = "brute-force-tab-save"; + #securityDefensesHeadersTab = "security-defenses-headers-tab"; + #securityDefensesBruteForceTab = "security-defenses-brute-force-tab"; + #clientProfileLink = 'table[aria-label="Profiles"] tbody a'; #realmName?: string; constructor(realmName?: string) { @@ -313,6 +319,10 @@ export default class RealmSettingsPage extends CommonPage { cy.get(this.#realmDisplayName).clear().type(displayName); } + clearRealmId() { + cy.get(this.#realmId).clear(); + } + fillFromDisplayName(displayName: string) { cy.findByTestId(this.#fromDisplayName).clear().type(displayName); } @@ -1043,6 +1053,11 @@ export default class RealmSettingsPage extends CommonPage { return this; } + searchNonExistingClientProfile(name: string) { + new ListingPage().searchItem(name, false); + return this; + } + shouldNotHaveConditionsConfigured() { cy.get(this.#clientPolicy).click(); cy.get('h2[class*="kc-emptyConditions"]').should( @@ -1250,6 +1265,24 @@ export default class RealmSettingsPage extends CommonPage { return this; } + saveSecurityDefensesHeaders() { + cy.findByTestId(this.#securityDefensesHeadersSaveBtn).click(); + } + + saveSecurityDefensesBruteForce() { + cy.findByTestId(this.#securityDefensesBruteForceSaveBtn).click(); + } + + goToSecurityDefensesHeadersTab() { + cy.findByTestId(this.#securityDefensesHeadersTab).click(); + return this; + } + + goToSecurityDefensesBruteForceTab() { + cy.findByTestId(this.#securityDefensesBruteForceTab).click(); + return this; + } + goToSessionsTab() { cy.findByTestId(this.sessionsTab).click(); return this; @@ -1259,4 +1292,9 @@ export default class RealmSettingsPage extends CommonPage { cy.findByTestId(this.tokensTab).click(); return this; } + + goToClientProfileByNameLink(profileName: string) { + cy.get(this.#clientProfileLink).contains(profileName).click(); + return this; + } } diff --git a/js/apps/admin-ui/package.json b/js/apps/admin-ui/package.json index e4283153ddbf..2178bd192a7a 100644 --- a/js/apps/admin-ui/package.json +++ b/js/apps/admin-ui/package.json @@ -73,7 +73,7 @@ "file-saver": "^2.0.5", "file-selector": "^0.6.0", "flat": "^6.0.1", - "i18next": "^23.7.8", + "i18next": "^23.7.9", "i18next-http-backend": "^2.4.2", "keycloak-js": "workspace:*", "lodash-es": "^4.17.21", @@ -97,7 +97,7 @@ "@types/dagre": "^0.7.52", "@types/file-saver": "^2.0.7", "@types/lodash-es": "^4.17.12", - "@types/react": "^18.2.43", + "@types/react": "^18.2.44", "@types/react-dom": "^18.2.17", "@types/uuid": "^9.0.7", "@vitejs/plugin-react-swc": "^3.5.0", @@ -108,7 +108,7 @@ "lightningcss": "^1.22.1", "ts-node": "^10.9.2", "uuid": "^9.0.1", - "vite": "^5.0.6", + "vite": "^5.0.8", "vite-plugin-checker": "^0.6.2", "vitest": "^1.0.4" } diff --git a/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx b/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx index 4ac8cd0e78b5..42084b7ca8ef 100644 --- a/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx +++ b/js/apps/admin-ui/src/clients/registration/ClientRegistrationList.tsx @@ -109,7 +109,10 @@ export const ClientRegistrationList = ({ loader={policies} toolbarItem={ - diff --git a/js/apps/admin-ui/src/realm-settings/security-defences/SecurityDefenses.tsx b/js/apps/admin-ui/src/realm-settings/security-defences/SecurityDefenses.tsx index 07632a02ae8c..c28c70c11781 100644 --- a/js/apps/admin-ui/src/realm-settings/security-defences/SecurityDefenses.tsx +++ b/js/apps/admin-ui/src/realm-settings/security-defences/SecurityDefenses.tsx @@ -22,6 +22,7 @@ export const SecurityDefenses = ({ realm, save }: SecurityDefensesProps) => { {t("headers")}} > @@ -31,6 +32,7 @@ export const SecurityDefenses = ({ realm, save }: SecurityDefensesProps) => { {t("bruteForceDetection")}} > diff --git a/js/libs/keycloak-js/package.json b/js/libs/keycloak-js/package.json index b9575a07b516..3f03452777a9 100644 --- a/js/libs/keycloak-js/package.json +++ b/js/libs/keycloak-js/package.json @@ -71,7 +71,7 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.5", "es6-promise": "^4.2.8", - "rollup": "^4.6.1", + "rollup": "^4.8.0", "shx": "^0.3.4" }, "dependencies": { diff --git a/js/libs/keycloak-masthead/package.json b/js/libs/keycloak-masthead/package.json index 0cecefb5f6b2..081af50ca54e 100644 --- a/js/libs/keycloak-masthead/package.json +++ b/js/libs/keycloak-masthead/package.json @@ -47,11 +47,11 @@ "react-dom": "^18.2.0" }, "devDependencies": { - "@types/react": "^18.2.43", + "@types/react": "^18.2.44", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react-swc": "^3.5.0", "rollup-plugin-peer-deps-external": "^2.2.4", - "vite": "^5.0.6", + "vite": "^5.0.8", "vite-plugin-checker": "^0.6.2", "vite-plugin-dts": "^3.6.4" } diff --git a/js/libs/ui-shared/package.json b/js/libs/ui-shared/package.json index 2023f2c225f7..8836e22c3ee8 100644 --- a/js/libs/ui-shared/package.json +++ b/js/libs/ui-shared/package.json @@ -43,7 +43,7 @@ "@keycloak/keycloak-admin-client": "workspace:*", "@patternfly/react-core": "^4.278.0", "@patternfly/react-icons": "^4.93.7", - "i18next": "^23.7.8", + "i18next": "^23.7.9", "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -52,11 +52,11 @@ }, "devDependencies": { "@types/lodash-es": "^4.17.12", - "@types/react": "^18.2.43", + "@types/react": "^18.2.44", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react-swc": "^3.5.0", "rollup-plugin-peer-deps-external": "^2.2.4", - "vite": "^5.0.6", + "vite": "^5.0.8", "vite-plugin-checker": "^0.6.2", "vite-plugin-dts": "^3.6.4", "vite-plugin-lib-inject-css": "^1.3.0", diff --git a/js/package.json b/js/package.json index 9efb7c10c8ac..57a8a67b91a9 100644 --- a/js/package.json +++ b/js/package.json @@ -6,8 +6,8 @@ }, "devDependencies": { "@types/node": "^20.10.4", - "@typescript-eslint/eslint-plugin": "^6.13.2", - "@typescript-eslint/parser": "^6.13.2", + "@typescript-eslint/eslint-plugin": "^6.14.0", + "@typescript-eslint/parser": "^6.14.0", "eslint": "^8.55.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", @@ -20,7 +20,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "prettier": "^3.1.0", + "prettier": "^3.1.1", "tslib": "^2.6.2", "typescript": "^5.3.3", "wireit": "^0.14.1" diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 58a9446f9970..f0f40c3caafa 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -12,11 +12,11 @@ importers: specifier: ^20.10.4 version: 20.10.4 '@typescript-eslint/eslint-plugin': - specifier: ^6.13.2 - version: 6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.3) + specifier: ^6.14.0 + version: 6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.55.0)(typescript@5.3.3) '@typescript-eslint/parser': - specifier: ^6.13.2 - version: 6.13.2(eslint@8.55.0)(typescript@5.3.3) + specifier: ^6.14.0 + version: 6.14.0(eslint@8.55.0)(typescript@5.3.3) eslint: specifier: ^8.55.0 version: 8.55.0 @@ -25,13 +25,13 @@ importers: version: 9.1.0(eslint@8.55.0) eslint-import-resolver-typescript: specifier: ^3.6.1 - version: 3.6.1(@typescript-eslint/parser@6.13.2)(eslint-plugin-import@2.29.0)(eslint@8.55.0) + version: 3.6.1(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.0)(eslint@8.55.0) eslint-plugin-cypress: specifier: ^2.15.1 version: 2.15.1(eslint@8.55.0) eslint-plugin-import: specifier: ^2.29.0 - version: 2.29.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + version: 2.29.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) eslint-plugin-lodash: specifier: ^7.4.0 version: 7.4.0(eslint@8.55.0) @@ -40,7 +40,7 @@ importers: version: 10.2.0(eslint@8.55.0) eslint-plugin-prettier: specifier: ^5.0.1 - version: 5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.0) + version: 5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.1) eslint-plugin-react: specifier: ^7.33.2 version: 7.33.2(eslint@8.55.0) @@ -54,8 +54,8 @@ importers: specifier: ^15.2.0 version: 15.2.0 prettier: - specifier: ^3.1.0 - version: 3.1.0 + specifier: ^3.1.1 + version: 3.1.1 tslib: specifier: ^2.6.2 version: 2.6.2 @@ -81,8 +81,8 @@ importers: specifier: ^4.113.6 version: 4.113.6(react-dom@18.2.0)(react@18.2.0) i18next: - specifier: ^23.7.8 - version: 23.7.8 + specifier: ^23.7.9 + version: 23.7.9 i18next-http-backend: specifier: ^2.4.2 version: 2.4.2 @@ -106,7 +106,7 @@ importers: version: 7.49.0(react@18.2.0) react-i18next: specifier: ^13.5.0 - version: 13.5.0(i18next@23.7.8)(react-dom@18.2.0)(react@18.2.0) + version: 13.5.0(i18next@23.7.9)(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.20.1 version: 6.20.1(react-dom@18.2.0)(react@18.2.0) @@ -124,23 +124,23 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/react': - specifier: ^18.2.43 - version: 18.2.43 + specifier: ^18.2.44 + version: 18.2.44 '@types/react-dom': specifier: ^18.2.17 version: 18.2.17 '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.5.0(vite@5.0.6) + version: 3.5.0(vite@5.0.8) lightningcss: specifier: ^1.22.1 version: 1.22.1 vite: - specifier: ^5.0.6 - version: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + specifier: ^5.0.8 + version: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vite-plugin-checker: specifier: ^0.6.2 - version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.6) + version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.8) apps/admin-ui: dependencies: @@ -167,7 +167,7 @@ importers: version: 4.113.6(react-dom@18.2.0)(react@18.2.0) admin-ui: specifier: 'file:' - version: file:apps/admin-ui(@types/react@18.2.43)(react-monaco-editor@0.51.0) + version: file:apps/admin-ui(@types/react@18.2.44)(react-monaco-editor@0.51.0) dagre: specifier: ^0.8.5 version: 0.8.5 @@ -181,8 +181,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 i18next: - specifier: ^23.7.8 - version: 23.7.8 + specifier: ^23.7.9 + version: 23.7.9 i18next-http-backend: specifier: ^2.4.2 version: 2.4.2 @@ -212,13 +212,13 @@ importers: version: 7.49.0(react@18.2.0) react-i18next: specifier: ^13.5.0 - version: 13.5.0(i18next@23.7.8)(react-dom@18.2.0)(react@18.2.0) + version: 13.5.0(i18next@23.7.9)(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.20.1 version: 6.20.1(react-dom@18.2.0)(react@18.2.0) reactflow: specifier: ^11.10.1 - version: 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + version: 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) ui-shared: specifier: workspace:* version: link:../../libs/ui-shared @@ -248,8 +248,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/react': - specifier: ^18.2.43 - version: 18.2.43 + specifier: ^18.2.44 + version: 18.2.44 '@types/react-dom': specifier: ^18.2.17 version: 18.2.17 @@ -258,7 +258,7 @@ importers: version: 9.0.7 '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.5.0(vite@5.0.6) + version: 3.5.0(vite@5.0.8) cypress: specifier: ^13.6.1 version: 13.6.1 @@ -281,11 +281,11 @@ importers: specifier: ^9.0.1 version: 9.0.1 vite: - specifier: ^5.0.6 - version: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + specifier: ^5.0.8 + version: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vite-plugin-checker: specifier: ^0.6.2 - version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.6) + version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.8) vitest: specifier: ^1.0.4 version: 1.0.4(@types/node@20.10.4)(jsdom@23.0.1)(lightningcss@1.22.1) @@ -365,25 +365,25 @@ importers: devDependencies: '@rollup/plugin-commonjs': specifier: ^25.0.7 - version: 25.0.7(rollup@4.6.1) + version: 25.0.7(rollup@4.8.0) '@rollup/plugin-inject': specifier: ^5.0.5 - version: 5.0.5(rollup@4.6.1) + version: 5.0.5(rollup@4.8.0) '@rollup/plugin-node-resolve': specifier: ^15.2.3 - version: 15.2.3(rollup@4.6.1) + version: 15.2.3(rollup@4.8.0) '@rollup/plugin-terser': specifier: ^0.4.4 - version: 0.4.4(rollup@4.6.1) + version: 0.4.4(rollup@4.8.0) '@rollup/plugin-typescript': specifier: ^11.1.5 - version: 11.1.5(rollup@4.6.1)(tslib@2.6.2)(typescript@5.3.3) + version: 11.1.5(rollup@4.8.0)(tslib@2.6.2)(typescript@5.3.3) es6-promise: specifier: ^4.2.8 version: 4.2.8 rollup: - specifier: ^4.6.1 - version: 4.6.1 + specifier: ^4.8.0 + version: 4.8.0 shx: specifier: ^0.3.4 version: 0.3.4 @@ -407,26 +407,26 @@ importers: version: 18.2.0(react@18.2.0) devDependencies: '@types/react': - specifier: ^18.2.43 - version: 18.2.43 + specifier: ^18.2.44 + version: 18.2.44 '@types/react-dom': specifier: ^18.2.17 version: 18.2.17 '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.5.0(vite@5.0.6) + version: 3.5.0(vite@5.0.8) rollup-plugin-peer-deps-external: specifier: ^2.2.4 - version: 2.2.4(rollup@4.6.1) + version: 2.2.4(rollup@4.8.0) vite: - specifier: ^5.0.6 - version: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + specifier: ^5.0.8 + version: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vite-plugin-checker: specifier: ^0.6.2 - version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.6) + version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.8) vite-plugin-dts: specifier: ^3.6.4 - version: 3.6.4(@types/node@20.10.4)(rollup@4.6.1)(typescript@5.3.3)(vite@5.0.6) + version: 3.6.4(@types/node@20.10.4)(rollup@4.8.0)(typescript@5.3.3)(vite@5.0.8) libs/ui-shared: dependencies: @@ -440,8 +440,8 @@ importers: specifier: ^4.93.7 version: 4.93.7(react-dom@18.2.0)(react@18.2.0) i18next: - specifier: ^23.7.8 - version: 23.7.8 + specifier: ^23.7.9 + version: 23.7.9 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -456,35 +456,35 @@ importers: version: 7.49.0(react@18.2.0) react-i18next: specifier: ^13.5.0 - version: 13.5.0(i18next@23.7.8)(react-dom@18.2.0)(react@18.2.0) + version: 13.5.0(i18next@23.7.9)(react-dom@18.2.0)(react@18.2.0) devDependencies: '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 '@types/react': - specifier: ^18.2.43 - version: 18.2.43 + specifier: ^18.2.44 + version: 18.2.44 '@types/react-dom': specifier: ^18.2.17 version: 18.2.17 '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.5.0(vite@5.0.6) + version: 3.5.0(vite@5.0.8) rollup-plugin-peer-deps-external: specifier: ^2.2.4 - version: 2.2.4(rollup@4.6.1) + version: 2.2.4(rollup@4.8.0) vite: - specifier: ^5.0.6 - version: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + specifier: ^5.0.8 + version: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vite-plugin-checker: specifier: ^0.6.2 - version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.6) + version: 0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.8) vite-plugin-dts: specifier: ^3.6.4 - version: 3.6.4(@types/node@20.10.4)(rollup@4.6.1)(typescript@5.3.3)(vite@5.0.6) + version: 3.6.4(@types/node@20.10.4)(rollup@4.8.0)(typescript@5.3.3)(vite@5.0.8) vite-plugin-lib-inject-css: specifier: ^1.3.0 - version: 1.3.0(vite@5.0.6) + version: 1.3.0(vite@5.0.8) vitest: specifier: ^1.0.4 version: 1.0.4(@types/node@20.10.4)(jsdom@23.0.1)(lightningcss@1.22.1) @@ -1102,7 +1102,7 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-dropzone: 9.0.0(react@18.2.0) - react-monaco-editor: 0.51.0(@types/react@18.2.43)(monaco-editor@0.45.0)(react@18.2.0) + react-monaco-editor: 0.51.0(@types/react@18.2.44)(monaco-editor@0.45.0)(react@18.2.0) tslib: 2.6.2 dev: false @@ -1177,39 +1177,39 @@ packages: playwright: 1.40.1 dev: true - /@reactflow/background@11.3.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/background@11.3.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-06FPlSUOOMALEEs+2PqPAbpqmL7WDjrkbG2UsDr2d6mbcDDhHiV4tu9FYoz44SQvXo7ma9VRotlsaR4OiRcYsg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/controls@11.2.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/controls@11.2.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-4QHT92/ACVlZkvV+Hq44bAPV8WbMhkJl+/J0EbXcqQ1+an7cWJsF84eeelJw7R5J76RoaSSpKdsWsL2v7HAVlw==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/core@11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/core@11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-GIh3usY1W3eVobx//OO9+Cwm+5evQBBdPGxDaeXwm25UqPMWRI240nXQA5F/5gL5Mwpf0DUC7DR2EmrKNQy+Rw==} peerDependencies: react: '>=17' @@ -1225,19 +1225,19 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/minimap@11.7.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/minimap@11.7.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-kJEtyeQkTZYViLGebVWHVUJROMAGcvejvT+iX4DqKnFb5yK8E8LWlXQpRx2FrL9gDy80mJJaciy7IxnnQKE1bg==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) '@types/d3-selection': 3.0.8 '@types/d3-zoom': 3.0.6 classcat: 5.0.4 @@ -1245,41 +1245,41 @@ packages: d3-zoom: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-resizer@2.2.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-resizer@2.2.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1Xb6q97uP7hRBLpog9sRCNfnsHdDgFRGEiU+lQqGgPEAeYwl4nRjWa/sXwH6ajniKxBhGEvrdzOgEFn6CRMcpQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 d3-drag: 3.0.0 d3-selection: 3.0.0 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer dev: false - /@reactflow/node-toolbar@1.3.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /@reactflow/node-toolbar@1.3.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JXDEuZ0wKjZ8z7qK2bIst0eZPzNyVEsiHL0e93EyuqT4fA9icoyE0fLq2ryNOOp7MXgId1h7LusnH6ta45F0yQ==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) classcat: 5.0.4 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - zustand: 4.4.6(@types/react@18.2.43)(react@18.2.0) + zustand: 4.4.6(@types/react@18.2.44)(react@18.2.0) transitivePeerDependencies: - '@types/react' - immer @@ -1290,7 +1290,7 @@ packages: engines: {node: '>=14.0.0'} dev: false - /@rollup/plugin-commonjs@25.0.7(rollup@4.6.1): + /@rollup/plugin-commonjs@25.0.7(rollup@4.8.0): resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1299,16 +1299,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.6.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) commondir: 1.0.1 estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.5 - rollup: 4.6.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-inject@5.0.5(rollup@4.6.1): + /@rollup/plugin-inject@5.0.5(rollup@4.8.0): resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1317,13 +1317,13 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.6.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) estree-walker: 2.0.2 magic-string: 0.30.5 - rollup: 4.6.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-node-resolve@15.2.3(rollup@4.6.1): + /@rollup/plugin-node-resolve@15.2.3(rollup@4.8.0): resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1332,16 +1332,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.6.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 - rollup: 4.6.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-terser@0.4.4(rollup@4.6.1): + /@rollup/plugin-terser@0.4.4(rollup@4.8.0): resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1350,13 +1350,13 @@ packages: rollup: optional: true dependencies: - rollup: 4.6.1 + rollup: 4.8.0 serialize-javascript: 6.0.1 smob: 1.4.1 terser: 5.24.0 dev: true - /@rollup/plugin-typescript@11.1.5(rollup@4.6.1)(tslib@2.6.2)(typescript@5.3.3): + /@rollup/plugin-typescript@11.1.5(rollup@4.8.0)(tslib@2.6.2)(typescript@5.3.3): resolution: {integrity: sha512-rnMHrGBB0IUEv69Q8/JGRD/n4/n6b3nfpufUu26axhUcboUzv/twfZU8fIBbTOphRAe0v8EyxzeDpKXqGHfyDA==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1369,14 +1369,14 @@ packages: tslib: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.6.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) resolve: 1.22.8 - rollup: 4.6.1 + rollup: 4.8.0 tslib: 2.6.2 typescript: 5.3.3 dev: true - /@rollup/pluginutils@5.0.5(rollup@4.6.1): + /@rollup/pluginutils@5.0.5(rollup@4.8.0): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: @@ -1388,99 +1388,107 @@ packages: '@types/estree': 1.0.4 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.6.1 + rollup: 4.8.0 dev: true - /@rollup/rollup-android-arm-eabi@4.6.1: - resolution: {integrity: sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==} + /@rollup/rollup-android-arm-eabi@4.8.0: + resolution: {integrity: sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.6.1: - resolution: {integrity: sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==} + /@rollup/rollup-android-arm64@4.8.0: + resolution: {integrity: sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.6.1: - resolution: {integrity: sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==} + /@rollup/rollup-darwin-arm64@4.8.0: + resolution: {integrity: sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.6.1: - resolution: {integrity: sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==} + /@rollup/rollup-darwin-x64@4.8.0: + resolution: {integrity: sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.6.1: - resolution: {integrity: sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==} + /@rollup/rollup-linux-arm-gnueabihf@4.8.0: + resolution: {integrity: sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.6.1: - resolution: {integrity: sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==} + /@rollup/rollup-linux-arm64-gnu@4.8.0: + resolution: {integrity: sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.6.1: - resolution: {integrity: sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==} + /@rollup/rollup-linux-arm64-musl@4.8.0: + resolution: {integrity: sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.6.1: - resolution: {integrity: sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==} + /@rollup/rollup-linux-riscv64-gnu@4.8.0: + resolution: {integrity: sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.8.0: + resolution: {integrity: sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.6.1: - resolution: {integrity: sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==} + /@rollup/rollup-linux-x64-musl@4.8.0: + resolution: {integrity: sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.6.1: - resolution: {integrity: sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==} + /@rollup/rollup-win32-arm64-msvc@4.8.0: + resolution: {integrity: sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.6.1: - resolution: {integrity: sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==} + /@rollup/rollup-win32-ia32-msvc@4.8.0: + resolution: {integrity: sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.6.1: - resolution: {integrity: sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==} + /@rollup/rollup-win32-x64-msvc@4.8.0: + resolution: {integrity: sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==} cpu: [x64] os: [win32] requiresBuild: true @@ -1990,11 +1998,11 @@ packages: /@types/react-dom@18.2.17: resolution: {integrity: sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==} dependencies: - '@types/react': 18.2.43 + '@types/react': 18.2.44 dev: true - /@types/react@18.2.43: - resolution: {integrity: sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==} + /@types/react@18.2.44: + resolution: {integrity: sha512-i7rRdWr8XsAdqVXU05NSlash6DapNLVoNnsfWfOYN2BUIzkH2YgagIeOuUK1fNmJ08iGHpt2enwi0beaBR+ldA==} dependencies: '@types/prop-types': 15.7.9 '@types/scheduler': 0.16.5 @@ -2044,8 +2052,8 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@6.13.2(@typescript-eslint/parser@6.13.2)(eslint@8.55.0)(typescript@5.3.3): - resolution: {integrity: sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==} + /@typescript-eslint/eslint-plugin@6.14.0(@typescript-eslint/parser@6.14.0)(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha @@ -2056,11 +2064,11 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.10.0 - '@typescript-eslint/parser': 6.13.2(eslint@8.55.0)(typescript@5.3.3) - '@typescript-eslint/scope-manager': 6.13.2 - '@typescript-eslint/type-utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) - '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.13.2 + '@typescript-eslint/parser': 6.14.0(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/type-utils': 6.14.0(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 graphemer: 1.4.0 @@ -2073,8 +2081,8 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.13.2(eslint@8.55.0)(typescript@5.3.3): - resolution: {integrity: sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==} + /@typescript-eslint/parser@6.14.0(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2083,10 +2091,10 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/scope-manager': 6.13.2 - '@typescript-eslint/types': 6.13.2 - '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) - '@typescript-eslint/visitor-keys': 6.13.2 + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 typescript: 5.3.3 @@ -2094,16 +2102,16 @@ packages: - supports-color dev: true - /@typescript-eslint/scope-manager@6.13.2: - resolution: {integrity: sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==} + /@typescript-eslint/scope-manager@6.14.0: + resolution: {integrity: sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.13.2 - '@typescript-eslint/visitor-keys': 6.13.2 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 dev: true - /@typescript-eslint/type-utils@6.13.2(eslint@8.55.0)(typescript@5.3.3): - resolution: {integrity: sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==} + /@typescript-eslint/type-utils@6.14.0(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2112,8 +2120,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) - '@typescript-eslint/utils': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) + '@typescript-eslint/utils': 6.14.0(eslint@8.55.0)(typescript@5.3.3) debug: 4.3.4(supports-color@8.1.1) eslint: 8.55.0 ts-api-utils: 1.0.3(typescript@5.3.3) @@ -2122,13 +2130,13 @@ packages: - supports-color dev: true - /@typescript-eslint/types@6.13.2: - resolution: {integrity: sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==} + /@typescript-eslint/types@6.14.0: + resolution: {integrity: sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==} engines: {node: ^16.0.0 || >=18.0.0} dev: true - /@typescript-eslint/typescript-estree@6.13.2(typescript@5.3.3): - resolution: {integrity: sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==} + /@typescript-eslint/typescript-estree@6.14.0(typescript@5.3.3): + resolution: {integrity: sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: typescript: '*' @@ -2136,8 +2144,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/types': 6.13.2 - '@typescript-eslint/visitor-keys': 6.13.2 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/visitor-keys': 6.14.0 debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 @@ -2148,8 +2156,8 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@6.13.2(eslint@8.55.0)(typescript@5.3.3): - resolution: {integrity: sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==} + /@typescript-eslint/utils@6.14.0(eslint@8.55.0)(typescript@5.3.3): + resolution: {integrity: sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 @@ -2157,9 +2165,9 @@ packages: '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) '@types/json-schema': 7.0.14 '@types/semver': 7.5.4 - '@typescript-eslint/scope-manager': 6.13.2 - '@typescript-eslint/types': 6.13.2 - '@typescript-eslint/typescript-estree': 6.13.2(typescript@5.3.3) + '@typescript-eslint/scope-manager': 6.14.0 + '@typescript-eslint/types': 6.14.0 + '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) eslint: 8.55.0 semver: 7.5.4 transitivePeerDependencies: @@ -2167,11 +2175,11 @@ packages: - typescript dev: true - /@typescript-eslint/visitor-keys@6.13.2: - resolution: {integrity: sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==} + /@typescript-eslint/visitor-keys@6.14.0: + resolution: {integrity: sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==} engines: {node: ^16.0.0 || >=18.0.0} dependencies: - '@typescript-eslint/types': 6.13.2 + '@typescript-eslint/types': 6.14.0 eslint-visitor-keys: 3.4.3 dev: true @@ -2179,13 +2187,13 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.5.0(vite@5.0.6): + /@vitejs/plugin-react-swc@3.5.0(vite@5.0.8): resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.3.96 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) transitivePeerDependencies: - '@swc/helpers' dev: true @@ -3533,7 +3541,7 @@ packages: - supports-color dev: true - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.2)(eslint-plugin-import@2.29.0)(eslint@8.55.0): + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.0)(eslint@8.55.0): resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3543,8 +3551,8 @@ packages: debug: 4.3.4(supports-color@8.1.1) enhanced-resolve: 5.15.0 eslint: 8.55.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) fast-glob: 3.3.1 get-tsconfig: 4.7.2 is-core-module: 2.13.1 @@ -3556,7 +3564,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -3577,11 +3585,11 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.55.0)(typescript@5.3.3) debug: 3.2.7(supports-color@8.1.1) eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.13.2)(eslint-plugin-import@2.29.0)(eslint@8.55.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.14.0)(eslint-plugin-import@2.29.0)(eslint@8.55.0) transitivePeerDependencies: - supports-color dev: true @@ -3595,7 +3603,7 @@ packages: globals: 13.23.0 dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -3605,7 +3613,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.13.2(eslint@8.55.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.14.0(eslint@8.55.0)(typescript@5.3.3) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 @@ -3614,7 +3622,7 @@ packages: doctrine: 2.1.0 eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.2)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.14.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -3651,7 +3659,7 @@ packages: rambda: 7.5.0 dev: true - /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.0): + /eslint-plugin-prettier@5.0.1(eslint-config-prettier@9.1.0)(eslint@8.55.0)(prettier@3.1.1): resolution: {integrity: sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3667,7 +3675,7 @@ packages: dependencies: eslint: 8.55.0 eslint-config-prettier: 9.1.0(eslint@8.55.0) - prettier: 3.1.0 + prettier: 3.1.1 prettier-linter-helpers: 1.0.0 synckit: 0.8.5 dev: true @@ -4438,8 +4446,8 @@ packages: - encoding dev: false - /i18next@23.7.8: - resolution: {integrity: sha512-yCe9964O+1abdIG01AOzk6P9mQi0HVJV1B57whYJQu6TjmrB9JHHDYonDI8amGt6M6b9bP3x3R0Zh7ROmvX7JQ==} + /i18next@23.7.9: + resolution: {integrity: sha512-wturtxTfJLJdLzHhyfxXo2l9Cbu2Iz4wF4065oWThPvdFJMUUG3fhXD3BLCHgrv4VxfScORq0i9sfCdjVPbgiw==} dependencies: '@babel/runtime': 7.23.2 dev: false @@ -5749,8 +5757,8 @@ packages: fast-diff: 1.3.0 dev: true - /prettier@3.1.0: - resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + /prettier@3.1.1: + resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} engines: {node: '>=14'} hasBin: true dev: true @@ -5928,7 +5936,7 @@ packages: react: 18.2.0 dev: false - /react-i18next@13.5.0(i18next@23.7.8)(react-dom@18.2.0)(react@18.2.0): + /react-i18next@13.5.0(i18next@23.7.9)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==} peerDependencies: i18next: '>= 23.2.3' @@ -5943,7 +5951,7 @@ packages: dependencies: '@babel/runtime': 7.23.2 html-parse-stringify: 3.0.1 - i18next: 23.7.8 + i18next: 23.7.9 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false @@ -5959,14 +5967,14 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true - /react-monaco-editor@0.51.0(@types/react@18.2.43)(monaco-editor@0.45.0)(react@18.2.0): + /react-monaco-editor@0.51.0(@types/react@18.2.44)(monaco-editor@0.45.0)(react@18.2.0): resolution: {integrity: sha512-6jx1V8p6gHVKJHFaTvicOtmlhFjOJhekobeNd92ZAo7F5UvAin1cF7bxWLCKgtxClYZ7CB3Ar284Kpbhj22FpQ==} peerDependencies: '@types/react': '>=17 <= 18' monaco-editor: ^0.34.1 react: '>=17 <= 18' dependencies: - '@types/react': 18.2.43 + '@types/react': 18.2.44 monaco-editor: 0.45.0 prop-types: 15.8.1 react: 18.2.0 @@ -6001,18 +6009,18 @@ packages: dependencies: loose-envify: 1.4.0 - /reactflow@11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0): + /reactflow@11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Q616fElAc5/N37tMwjuRkkgm/VgmnLLTNNCj61z5mvJxae+/VXZQMfot1K6a5LLz9G3SVKqU97PMb9Ga1PRXew==} peerDependencies: react: '>=17' react-dom: '>=17' dependencies: - '@reactflow/background': 11.3.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/controls': 11.2.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/core': 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/minimap': 11.7.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-resizer': 2.2.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) - '@reactflow/node-toolbar': 1.3.6(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/background': 11.3.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/controls': 11.2.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/core': 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/minimap': 11.7.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-resizer': 2.2.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) + '@reactflow/node-toolbar': 1.3.6(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) transitivePeerDependencies: @@ -6164,31 +6172,32 @@ packages: glob: 7.2.3 dev: true - /rollup-plugin-peer-deps-external@2.2.4(rollup@4.6.1): + /rollup-plugin-peer-deps-external@2.2.4(rollup@4.8.0): resolution: {integrity: sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==} peerDependencies: rollup: '*' dependencies: - rollup: 4.6.1 + rollup: 4.8.0 dev: true - /rollup@4.6.1: - resolution: {integrity: sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==} + /rollup@4.8.0: + resolution: {integrity: sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.6.1 - '@rollup/rollup-android-arm64': 4.6.1 - '@rollup/rollup-darwin-arm64': 4.6.1 - '@rollup/rollup-darwin-x64': 4.6.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.6.1 - '@rollup/rollup-linux-arm64-gnu': 4.6.1 - '@rollup/rollup-linux-arm64-musl': 4.6.1 - '@rollup/rollup-linux-x64-gnu': 4.6.1 - '@rollup/rollup-linux-x64-musl': 4.6.1 - '@rollup/rollup-win32-arm64-msvc': 4.6.1 - '@rollup/rollup-win32-ia32-msvc': 4.6.1 - '@rollup/rollup-win32-x64-msvc': 4.6.1 + '@rollup/rollup-android-arm-eabi': 4.8.0 + '@rollup/rollup-android-arm64': 4.8.0 + '@rollup/rollup-darwin-arm64': 4.8.0 + '@rollup/rollup-darwin-x64': 4.8.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.8.0 + '@rollup/rollup-linux-arm64-gnu': 4.8.0 + '@rollup/rollup-linux-arm64-musl': 4.8.0 + '@rollup/rollup-linux-riscv64-gnu': 4.8.0 + '@rollup/rollup-linux-x64-gnu': 4.8.0 + '@rollup/rollup-linux-x64-musl': 4.8.0 + '@rollup/rollup-win32-arm64-msvc': 4.8.0 + '@rollup/rollup-win32-ia32-msvc': 4.8.0 + '@rollup/rollup-win32-x64-msvc': 4.8.0 fsevents: 2.3.3 dev: true @@ -7013,7 +7022,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) pathe: 1.1.1 picocolors: 1.0.0 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) transitivePeerDependencies: - '@types/node' - less @@ -7025,7 +7034,7 @@ packages: - terser dev: true - /vite-plugin-checker@0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.6): + /vite-plugin-checker@0.6.2(eslint@8.55.0)(typescript@5.3.3)(vite@5.0.8): resolution: {integrity: sha512-YvvvQ+IjY09BX7Ab+1pjxkELQsBd4rPhWNw8WLBeFVxu/E7O+n6VYAqNsKdK/a2luFlX/sMpoWdGFfg4HvwdJQ==} engines: {node: '>=14.16'} peerDependencies: @@ -7071,14 +7080,14 @@ packages: strip-ansi: 6.0.1 tiny-invariant: 1.3.1 typescript: 5.3.3 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vscode-languageclient: 7.0.0 vscode-languageserver: 7.0.0 vscode-languageserver-textdocument: 1.0.11 vscode-uri: 3.0.8 dev: true - /vite-plugin-dts@3.6.4(@types/node@20.10.4)(rollup@4.6.1)(typescript@5.3.3)(vite@5.0.6): + /vite-plugin-dts@3.6.4(@types/node@20.10.4)(rollup@4.8.0)(typescript@5.3.3)(vite@5.0.8): resolution: {integrity: sha512-yOVhUI/kQhtS6lCXRYYLv2UUf9bftcwQK9ROxCX2ul17poLQs02ctWX7+vXB8GPRzH8VCK3jebEFtPqqijXx6w==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -7089,12 +7098,12 @@ packages: optional: true dependencies: '@microsoft/api-extractor': 7.38.2(@types/node@20.10.4) - '@rollup/pluginutils': 5.0.5(rollup@4.6.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) '@vue/language-core': 1.8.22(typescript@5.3.3) debug: 4.3.4(supports-color@8.1.1) kolorist: 1.8.0 typescript: 5.3.3 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vue-tsc: 1.8.22(typescript@5.3.3) transitivePeerDependencies: - '@types/node' @@ -7102,18 +7111,18 @@ packages: - supports-color dev: true - /vite-plugin-lib-inject-css@1.3.0(vite@5.0.6): + /vite-plugin-lib-inject-css@1.3.0(vite@5.0.8): resolution: {integrity: sha512-Rldq36U9TDlpDom3yoLybfJtzn897+oMKdX0+fxbVYnYjRGnTtaFfnMmfOckH8GQ3cvGAKv9Ai1PWyE0amIbjg==} peerDependencies: vite: '*' dependencies: magic-string: 0.30.5 picocolors: 1.0.0 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) dev: true - /vite@5.0.6(@types/node@20.10.4)(lightningcss@1.22.1): - resolution: {integrity: sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==} + /vite@5.0.8(@types/node@20.10.4)(lightningcss@1.22.1): + resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: @@ -7144,7 +7153,7 @@ packages: esbuild: 0.19.5 lightningcss: 1.22.1 postcss: 8.4.32 - rollup: 4.6.1 + rollup: 4.8.0 optionalDependencies: fsevents: 2.3.3 dev: true @@ -7194,7 +7203,7 @@ packages: strip-literal: 1.3.0 tinybench: 2.5.1 tinypool: 0.8.1 - vite: 5.0.6(@types/node@20.10.4)(lightningcss@1.22.1) + vite: 5.0.8(@types/node@20.10.4)(lightningcss@1.22.1) vite-node: 1.0.4(@types/node@20.10.4)(lightningcss@1.22.1) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -7534,7 +7543,7 @@ packages: commander: 9.5.0 dev: true - /zustand@4.4.6(@types/react@18.2.43)(react@18.2.0): + /zustand@4.4.6(@types/react@18.2.44)(react@18.2.0): resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==} engines: {node: '>=12.7.0'} peerDependencies: @@ -7549,12 +7558,12 @@ packages: react: optional: true dependencies: - '@types/react': 18.2.43 + '@types/react': 18.2.44 react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) dev: false - file:apps/admin-ui(@types/react@18.2.43)(react-monaco-editor@0.51.0): + file:apps/admin-ui(@types/react@18.2.44)(react-monaco-editor@0.51.0): resolution: {directory: apps/admin-ui, type: directory} id: file:apps/admin-ui name: admin-ui @@ -7570,7 +7579,7 @@ packages: file-saver: 2.0.5 file-selector: 0.6.0 flat: 6.0.1 - i18next: 23.7.8 + i18next: 23.7.9 i18next-http-backend: 2.4.2 keycloak-js: link:libs/keycloak-js lodash-es: 4.17.21 @@ -7580,9 +7589,9 @@ packages: react-dropzone: 14.2.3(react@18.2.0) react-error-boundary: 3.1.4(react@18.2.0) react-hook-form: 7.49.0(react@18.2.0) - react-i18next: 13.5.0(i18next@23.7.8)(react-dom@18.2.0)(react@18.2.0) + react-i18next: 13.5.0(i18next@23.7.9)(react-dom@18.2.0)(react@18.2.0) react-router-dom: 6.20.1(react-dom@18.2.0)(react@18.2.0) - reactflow: 11.10.1(@types/react@18.2.43)(react-dom@18.2.0)(react@18.2.0) + reactflow: 11.10.1(@types/react@18.2.44)(react-dom@18.2.0)(react@18.2.0) ui-shared: link:libs/ui-shared use-react-router-breadcrumbs: 4.0.1(react-router-dom@6.20.1)(react@18.2.0) transitivePeerDependencies: diff --git a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java index 3b8be05a4605..61b6ffe4d5ac 100644 --- a/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java +++ b/rest/admin-ui-ext/src/main/java/org/keycloak/admin/ui/rest/UserResource.java @@ -1,11 +1,15 @@ package org.keycloak.admin.ui.rest; +import static java.util.Collections.emptyList; +import static java.util.Optional.ofNullable; import static org.keycloak.userprofile.UserProfileContext.USER_API; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @@ -18,6 +22,7 @@ import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.utils.StringUtil; /** * @author Pedro Igor @@ -42,7 +47,7 @@ public Map> getUnmanagedAttributes() { if (provider.isEnabled(realm)) { UserProfile profile = provider.create(USER_API, user); - Map> managedAttributes = profile.getAttributes().getReadable(false); + Map> managedAttributes = profile.getAttributes().getReadable(); Map> attributes = new HashMap<>(user.getAttributes()); UPConfig upConfig = provider.getConfiguration(); @@ -56,10 +61,10 @@ public Map> getUnmanagedAttributes() { attributes.remove(UserModel.USERNAME); attributes.remove(UserModel.EMAIL); - attributes.remove(UserModel.FIRST_NAME); - attributes.remove(UserModel.LAST_NAME); - return attributes; + return attributes.entrySet().stream() + .filter(entry -> ofNullable(entry.getValue()).orElse(emptyList()).stream().anyMatch(StringUtil::isNotBlank)) + .collect(Collectors.toMap(Entry::getKey, Entry::getValue)); } return Collections.emptyMap(); diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java index 9c6113142b36..c6015b800fea 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultAttributes.java @@ -19,6 +19,8 @@ package org.keycloak.userprofile; +import static java.util.Collections.emptyList; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -29,6 +31,8 @@ import java.util.Set; import java.util.function.Consumer; import java.util.stream.Collectors; +import java.util.stream.Stream; + import org.jboss.logging.Logger; import org.keycloak.common.util.CollectionUtil; import org.keycloak.models.Constants; @@ -38,6 +42,7 @@ import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy; +import org.keycloak.utils.StringUtil; import org.keycloak.validate.ValidationContext; import org.keycloak.validate.ValidationError; @@ -68,7 +73,7 @@ public class DefaultAttributes extends HashMap> implements private final Map metadataByAttribute; private final UPConfig upConfig; protected final UserModel user; - private Map> unmanagedAttributes = new HashMap<>(); + private final Map> unmanagedAttributes = new HashMap<>(); public DefaultAttributes(UserProfileContext context, Map attributes, UserModel user, UserProfileMetadata profileMetadata, @@ -82,28 +87,28 @@ public DefaultAttributes(UserProfileContext context, Map attributes, } @Override - public boolean isReadOnly(String attributeName) { - if (!isManagedAttribute(attributeName)) { + public boolean isReadOnly(String name) { + if (!isManagedAttribute(name)) { return !isAllowEditUnmanagedAttribute(); } - if (UserModel.USERNAME.equals(attributeName)) { + if (UserModel.USERNAME.equals(name)) { if (isServiceAccountUser()) { return true; } } - if (UserModel.EMAIL.equals(attributeName)) { + if (UserModel.EMAIL.equals(name)) { if (isServiceAccountUser()) { return false; } } - if (isReadOnlyFromMetadata(attributeName) || isReadOnlyInternalAttribute(attributeName)) { + if (isReadOnlyFromMetadata(name) || isReadOnlyInternalAttribute(name)) { return true; } - return getMetadata(attributeName) == null; + return getMetadata(name) == null; } private boolean isAllowEditUnmanagedAttribute() { @@ -156,9 +161,9 @@ public boolean validate(String name, Consumer... listeners) { List metadatas = new ArrayList<>(); metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(attribute.getKey())) - .map(Collections::singletonList).orElse(Collections.emptyList())); + .map(Collections::singletonList).orElse(emptyList())); metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY)) - .map(Collections::singletonList).orElse(Collections.emptyList())); + .map(Collections::singletonList).orElse(emptyList())); Boolean result = null; @@ -172,12 +177,15 @@ public boolean validate(String name, Consumer... listeners) { continue; } - if (user != null && metadata.isReadOnly(attributeContext) - && CollectionUtil.collectionEquals(user.getAttributeStream(name).collect(Collectors.toList()), attribute.getValue())) { - // allow update if the value was already wrong in the user and is read-only in this context - logger.warnf("User '%s' attribute '%s' has previous validation errors %s but is read-only in context %s.", - user.getUsername(), name, vc.getErrors(), attributeContext.getContext()); - continue; + if (user != null && metadata.isReadOnly(attributeContext)) { + List value = user.getAttributeStream(name).filter(StringUtil::isNotBlank).collect(Collectors.toList()); + List newValue = attribute.getValue().stream().filter(StringUtil::isNotBlank).collect(Collectors.toList()); + if (CollectionUtil.collectionEquals(value, newValue)) { + // allow update if the value was already wrong in the user and is read-only in this context + logger.debugf("User '%s' attribute '%s' has previous validation errors %s but is read-only in context %s.", + user.getUsername(), name, vc.getErrors(), attributeContext.getContext()); + continue; + } } if (result == null) { @@ -198,7 +206,7 @@ public boolean validate(String name, Consumer... listeners) { } @Override - public List getValues(String name) { + public List get(String name) { return getOrDefault(name, EMPTY_VALUE); } @@ -236,21 +244,7 @@ public Map> getWritable() { @Override public AttributeMetadata getMetadata(String name) { if (unmanagedAttributes.containsKey(name)) { - return new AttributeMetadata(name, Integer.MAX_VALUE) { - final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy(); - - @Override - public boolean canView(AttributeContext context) { - return canEdit(context) - || (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext())); - } - - @Override - public boolean canEdit(AttributeContext context) { - return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy) - || (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext())); - } - }; + return createUnmanagedAttributeMetadata(name); } return Optional.ofNullable(metadataByAttribute.get(name)) @@ -265,9 +259,14 @@ public Map> getReadable() { for (String name : nameSet()) { AttributeMetadata metadata = getMetadata(name); - if (metadata == null - || !metadata.canView(createAttributeContext(metadata)) - || !metadata.isSelected(createAttributeContext(metadata))) { + if (metadata == null) { + attributes.remove(name); + continue; + } + + AttributeContext attributeContext = createAttributeContext(metadata); + + if (!metadata.canView(attributeContext) || !metadata.isSelected(attributeContext)) { attributes.remove(name); } } @@ -277,7 +276,7 @@ public Map> getReadable() { @Override public Map> toMap() { - return this; + return Collections.unmodifiableMap(this); } protected boolean isServiceAccountUser() { @@ -342,7 +341,7 @@ private Map> normalizeAttributes(Map attributes) if (!isSupportedAttribute(key)) { if (!isManagedAttribute(key) && isAllowUnmanagedAttribute()) { - unmanagedAttributes.put(key, (List) entry.getValue()); + unmanagedAttributes.put(key, normalizeAttributeValues(key, entry.getValue())); } continue; } @@ -351,18 +350,7 @@ private Map> normalizeAttributes(Map attributes) key = key.substring(Constants.USER_ATTRIBUTES_PREFIX.length()); } - Object value = entry.getValue(); - List values; - - if (value instanceof String) { - values = Collections.singletonList((String) value); - } else { - values = (List) value; - } - - if (UserModel.USERNAME.equals(key) || UserModel.EMAIL.equals(key)) { - values = values.stream().map(KeycloakModelUtils::toLowerCaseSafe).collect(Collectors.toList()); - } + List values = normalizeAttributeValues(key, entry.getValue()); newAttributes.put(key, Collections.unmodifiableList(values)); } @@ -378,28 +366,24 @@ private Map> normalizeAttributes(Map attributes) AttributeMetadata metadata = metadataByAttribute.get(attributeName); if (user != null && isIncludeAttributeIfNotProvided(metadata)) { - values = user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE); + values = normalizeAttributeValues(attributeName, user.getAttributes().getOrDefault(attributeName, EMPTY_VALUE)); } newAttributes.put(attributeName, values); } if (user != null) { - List username = newAttributes.getOrDefault(UserModel.USERNAME, Collections.emptyList()); + List username = newAttributes.getOrDefault(UserModel.USERNAME, emptyList()); if (username.isEmpty() && isReadOnly(UserModel.USERNAME)) { setUserName(newAttributes, Collections.singletonList(user.getUsername())); } } - List email = newAttributes.getOrDefault(UserModel.EMAIL, Collections.emptyList()); + List email = newAttributes.getOrDefault(UserModel.EMAIL, emptyList()); if (!email.isEmpty() && realm.isRegistrationEmailAsUsername()) { - List lowerCaseEmailList = email.stream() - .filter(Objects::nonNull) - .collect(Collectors.toList()); - - setUserName(newAttributes, lowerCaseEmailList); + setUserName(newAttributes, email); if (user != null && isReadOnly(UserModel.EMAIL)) { newAttributes.put(UserModel.EMAIL, Collections.singletonList(user.getEmail())); @@ -414,6 +398,24 @@ private Map> normalizeAttributes(Map attributes) return newAttributes; } + private List normalizeAttributeValues(String name, Object value) { + List values; + + if (value instanceof String) { + values = Collections.singletonList((String) value); + } else { + values = (List) value; + } + + Stream valuesStream = Optional.ofNullable(values).orElse(EMPTY_VALUE).stream().filter(Objects::nonNull); + + if (UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name)) { + valuesStream = valuesStream.map(KeycloakModelUtils::toLowerCaseSafe); + } + + return valuesStream.collect(Collectors.toList()); + } + private boolean isAllowUnmanagedAttribute() { UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy(); @@ -466,12 +468,7 @@ protected boolean isSupportedAttribute(String name) { return true; } - if (isReadOnlyInternalAttribute(name)) { - return true; - } - - // checks whether the attribute is a core attribute - return isRootAttribute(name); + return isReadOnlyInternalAttribute(name); } private boolean isManagedAttribute(String name) { @@ -511,4 +508,22 @@ protected boolean isReadOnlyInternalAttribute(String attributeName) { public Map> getUnmanagedAttributes() { return unmanagedAttributes; } + + private AttributeMetadata createUnmanagedAttributeMetadata(String name) { + return new AttributeMetadata(name, Integer.MAX_VALUE) { + final UnmanagedAttributePolicy unmanagedAttributePolicy = upConfig.getUnmanagedAttributePolicy(); + + @Override + public boolean canView(AttributeContext context) { + return canEdit(context) + || (UnmanagedAttributePolicy.ADMIN_VIEW.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext())); + } + + @Override + public boolean canEdit(AttributeContext context) { + return UnmanagedAttributePolicy.ENABLED.equals(unmanagedAttributePolicy) + || (UnmanagedAttributePolicy.ADMIN_EDIT.equals(unmanagedAttributePolicy) && UserProfileContext.USER_API.equals(context.getContext())); + } + }; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java index 92792cc83005..1c1ad6a31b78 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/DefaultUserProfile.java @@ -19,6 +19,10 @@ package org.keycloak.userprofile; +import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata; +import static org.keycloak.userprofile.UserProfileUtil.isRootAttribute; + +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -31,7 +35,10 @@ import org.keycloak.common.util.CollectionUtil; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelException; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.AbstractUserRepresentation; import org.keycloak.storage.ReadOnlyException; import org.keycloak.utils.StringUtil; @@ -45,10 +52,11 @@ */ public final class DefaultUserProfile implements UserProfile { - protected final UserProfileMetadata metadata; + private final UserProfileMetadata metadata; private final Function userSupplier; private final Attributes attributes; private final KeycloakSession session; + private final boolean isUserProfileEnabled; private boolean validated; private UserModel user; @@ -59,6 +67,8 @@ public DefaultUserProfile(UserProfileMetadata metadata, Attributes attributes, F this.attributes = attributes; this.user = user; this.session = session; + UserProfileProvider provider = session.getProvider(UserProfileProvider.class); + isUserProfileEnabled = provider.isEnabled(session.getContext().getRealm()); } @Override @@ -144,16 +154,27 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri attrsToRemove.removeAll(attributes.nameSet()); - for (String attr : attrsToRemove) { - if (attributes.isReadOnly(attr)) { + for (String name : attrsToRemove) { + if (attributes.isReadOnly(name)) { continue; } - List currentValue = user.getAttributeStream(attr).filter(Objects::nonNull).collect(Collectors.toList()); - user.removeAttribute(attr); + List currentValue = user.getAttributeStream(name).filter(Objects::nonNull).collect(Collectors.toList()); + + if (isRootAttribute(name)) { + if (UserModel.FIRST_NAME.equals(name)) { + user.setFirstName(null); + } else if (UserModel.LAST_NAME.equals(name)) { + user.setLastName(null); + } else if (UserModel.LOCALE.equals(name)) { + user.removeAttribute(name); + } + } else { + user.removeAttribute(name); + } for (AttributeChangeListener listener : changeListener) { - listener.onChange(attr, user, currentValue); + listener.onChange(name, user, currentValue); } } } @@ -168,11 +189,88 @@ private UserModel updateInternal(UserModel user, boolean removeAttributes, Attri } private boolean isCustomAttribute(String name) { - return !getAttributes().isRootAttribute(name); + return !isRootAttribute(name); } @Override public Attributes getAttributes() { return attributes; } + + @Override + public R toRepresentation() { + if (user == null) { + throw new IllegalStateException("Can not create the representation because the user is not yet created"); + } + + R rep = createUserRepresentation(); + Map> readable = attributes.getReadable(); + Map> attributesRep = new HashMap<>(readable); + + // all the attributes here have read access and might be available in the representation + for (String name : readable.keySet()) { + List values = attributesRep.getOrDefault(name, Collections.emptyList()) + .stream().filter(StringUtil::isNotBlank) + .collect(Collectors.toList()); + + if (values.isEmpty()) { + // make sure empty attributes are not in the representation + attributesRep.remove(name); + continue; + } + + if (isRootAttribute(name)) { + if (UserModel.LOCALE.equals(name)) { + // local is a special root attribute as it does not have a field in the user representation + // it should be available as a regular attribute if set + continue; + } + + boolean isUnmanagedAttribute = isUserProfileEnabled && metadata.getAttribute(name).isEmpty(); + String value = isUnmanagedAttribute ? null : values.stream().findFirst().orElse(null); + + if (UserModel.USERNAME.equals(name)) { + rep.setUsername(value); + } else if (UserModel.EMAIL.equals(name)) { + rep.setEmail(value); + rep.setEmailVerified(user.isEmailVerified()); + } else if (UserModel.FIRST_NAME.equals(name)) { + rep.setFirstName(value); + } else if (UserModel.LAST_NAME.equals(name)) { + rep.setLastName(value); + } + + // we don't have root attributes as a regular attribute in the representation as they have their own fields + attributesRep.remove(name); + } + } + + rep.setId(user.getId()); + rep.setAttributes(attributesRep.isEmpty() ? null : attributesRep); + rep.setUserProfileMetadata(createUserProfileMetadata(session, this)); + + return rep; + } + + @SuppressWarnings("unchecked") + private R createUserRepresentation() { + UserProfileContext context = metadata.getContext(); + R rep; + + if (UserProfileContext.USER_API.equals(context)) { + RealmModel realm = session.getContext().getRealm(); + rep = (R) ModelToRepresentation.toRepresentation(session, realm, user); + } else { + // by default, we build the simplest representation without exposing much information about users + rep = (R) new org.keycloak.representations.account.UserRepresentation(); + } + + // reset the root attribute values so that they are calculated based on the user profile configuration + rep.setUsername(null); + rep.setEmail(null); + rep.setFirstName(null); + rep.setLastName(null); + + return rep; + } } diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java index 34c9ee05c514..761ae31004c2 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfile.java @@ -17,22 +17,38 @@ package org.keycloak.userprofile; -import java.util.function.BiConsumer; +import java.util.List; +import java.util.Map; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.AbstractUserRepresentation; /** - *

An interface providing as an entry point for managing users. + *

An interface that serves an entry point for managing users and their attributes. * - *

A {@code UserProfile} provides a manageable view for user information that also takes into account the context where it is being used. - * The context represents the different places in Keycloak where users are created, updated, or validated. + *

A {@code UserProfile} provides methods for creating, and updating users as well as for accessing their attributes. + * All its operations are based the {@link UserProfileContext}. By taking the context into account, the state and behavior of + * {@link UserProfile} instances depend on the context they are associated with where creating, updating, validating, and + * accessing the attribute set of a user is based on the configuration (see {@link org.keycloak.representations.userprofile.config.UPConfig}) + * and the constraints associated with a given context. + * + *

The {@link UserProfileContext} represents the different areas in Keycloak where users, and their attributes are managed. * Examples of contexts are: managing users through the Admin API, or through the Account API. * - *

By taking the context into account, the state and behavior of {@link UserProfile} instances depend on the context they - * are associated with, where validating, updating, creating, or obtaining representations of users is based on the configuration - * and constraints associated with a context. + *

A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}: + * + *

 {@code
+ * // resolve an existing user
+ * UserModel user = getExistingUser();
+ * // obtain the user profile provider
+ * UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
+ * // create a instance for managing the user profile through the USER_API context
+ * UserProfile profile = provider.create(USER_API, user);
+ * }
* - *

A {@code UserProfile} instance can be obtained through the {@link UserProfileProvider}. + *

The {@link UserProfileProvider} provides different methods for creating {@link UserProfile} instances, each one + * target for a specific scenario such as creating a new user, updating an existing one, or only for accessing the attributes + * for an existing user as shown in the above example. * * @see UserProfileContext * @see UserProfileProvider @@ -69,20 +85,23 @@ public interface UserProfile { void update(boolean removeAttributes, AttributeChangeListener... changeListener) throws ValidationException; /** - *

The same as {@link #update(boolean, BiConsumer[])} but forcing the removal of attributes. + *

The same as {@link #update(boolean, AttributeChangeListener...)}} but forcing the removal of attributes. * * @param changeListener a set of one or more listeners to listen for attribute changes * @throws ValidationException in case of any validation error */ - default void update(AttributeChangeListener... changeListener) throws ValidationException, RuntimeException { + default void update(AttributeChangeListener... changeListener) throws ValidationException { update(true, changeListener); } /** * Returns the attributes associated with this instance. Note that the attributes returned by this method are not necessarily - * the same from the {@link UserModel}, but those that should be validated and possibly updated to the {@link UserModel}. + * the same from the {@link UserModel} as they are based on the configurations set in the {@link org.keycloak.representations.userprofile.config.UPConfig} and + * the context this instance is based on. * * @return the attributes associated with this instance. */ Attributes getAttributes(); + + R toRepresentation(); } diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java index cfa6e0f4bd65..45059631accb 100644 --- a/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/UserProfileUtil.java @@ -20,11 +20,23 @@ package org.keycloak.userprofile; import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserModel; +import org.keycloak.provider.ConfiguredProvider; +import org.keycloak.representations.idm.UserProfileAttributeGroupMetadata; +import org.keycloak.representations.idm.UserProfileAttributeMetadata; import org.keycloak.representations.userprofile.config.UPConfig; +import org.keycloak.representations.userprofile.config.UPGroup; +import org.keycloak.validate.Validators; /** * @author Marek Posolda @@ -82,4 +94,70 @@ public static boolean addMetadataAttributeToUserProfile(String attrName, UserPro return true; } } + + /** + * Returns whether the attribute with the given {@code name} is a root attribute. + * + * @param name the attribute name + * @return + */ + public static boolean isRootAttribute(String name) { + return UserModel.USERNAME.equals(name) + || UserModel.EMAIL.equals(name) + || UserModel.FIRST_NAME.equals(name) + || UserModel.LAST_NAME.equals(name) + || UserModel.LOCALE.equals(name); + } + + public static org.keycloak.representations.idm.UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) { + Attributes profileAttributes = profile.getAttributes(); + Map> am = profileAttributes.getReadable(); + + if(am == null) + return null; + Map> unmanagedAttributes = profileAttributes.getUnmanagedAttributes(); + + List attributes = am.keySet().stream() + .map(profileAttributes::getMetadata) + .filter(Objects::nonNull) + .filter(attributeMetadata -> !unmanagedAttributes.containsKey(attributeMetadata.getName())) + .sorted(Comparator.comparingInt(AttributeMetadata::getGuiOrder)) + .map(sam -> toRestMetadata(sam, session, profile)) + .collect(Collectors.toList()); + + UserProfileProvider provider = session.getProvider(UserProfileProvider.class); + UPConfig config = provider.getConfiguration(); + + List groups = config.getGroups().stream().map(new Function() { + @Override + public UserProfileAttributeGroupMetadata apply(UPGroup upGroup) { + return new UserProfileAttributeGroupMetadata(upGroup.getName(), upGroup.getDisplayHeader(), upGroup.getDisplayDescription(), upGroup.getAnnotations()); + } + }).collect(Collectors.toList()); + + return new org.keycloak.representations.idm.UserProfileMetadata(attributes, groups); + } + + private static UserProfileAttributeMetadata toRestMetadata(AttributeMetadata am, KeycloakSession session, UserProfile profile) { + String group = null; + + if (am.getAttributeGroupMetadata() != null) { + group = am.getAttributeGroupMetadata().getName(); + } + + return new UserProfileAttributeMetadata(am.getName(), + am.getAttributeDisplayName(), + profile.getAttributes().isRequired(am.getName()), + profile.getAttributes().isReadOnly(am.getName()), + group, + am.getAnnotations(), + toValidatorMetadata(am, session)); + } + + private static Map> toValidatorMetadata(AttributeMetadata am, KeycloakSession session){ + // we return only validators which are instance of ConfiguredProvider. Others are expected as internal. + return am.getValidators() == null ? null : am.getValidators().stream() + .filter(avm -> (Validators.validator(session, avm.getValidatorId()) instanceof ConfiguredProvider)) + .collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig)); + } } diff --git a/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java b/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java index af5a560b0a37..a55da0e31bcc 100644 --- a/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java +++ b/server-spi/src/main/java/org/keycloak/userprofile/Attributes.java @@ -19,20 +19,38 @@ package org.keycloak.userprofile; +import static java.util.Optional.ofNullable; + import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; -import org.keycloak.models.UserModel; import org.keycloak.validate.ValidationError; /** *

This interface wraps the attributes associated with a user profile. Different operations are provided to access and * manage these attributes. * + *

Attributes are classified as:

+ *
    + *
  • Managed + *
  • Unmanaged + *
+ * + *

A managed attribute is any attribute defined in the user profile configuration. Therefore, they are known by + * the server and can be managed accordingly. + * + *

A unmanaged attributes is any attribute not defined in the user profile configuration. Therefore, the server + * does not know about them and they cannot use capabilities provided by the server. However, they can still be managed by + * administrators by setting any of the {@link org.keycloak.representations.userprofile.config.UPConfig.UnmanagedAttributePolicy}. + * + *

Any attribute available from this interface has a corresponding {@link AttributeMetadata}

. The metadata describes + * the settings for a given attribute so that the server can communicate to a caller the constraints + * (see {@link org.keycloak.representations.userprofile.config.UPConfig} and the availability of the attribute in + * a given {@link UserProfileContext}. + * * @author Pedro Igor */ public interface Attributes { @@ -49,8 +67,8 @@ public interface Attributes { * * @return the first value */ - default String getFirstValue(String name) { - List values = getValues(name); + default String getFirst(String name) { + List values = ofNullable(get(name)).orElse(List.of()); if (values.isEmpty()) { return null; @@ -66,16 +84,16 @@ default String getFirstValue(String name) { * * @return the attribute values */ - List getValues(String name); + List get(String name); /** * Checks whether an attribute is read-only. * - * @param key + * @param name the attribute name * - * @return + * @return {@code true} if the attribute is read-only. Otherwise, {@code false} */ - boolean isReadOnly(String key); + boolean isReadOnly(String name); /** * Validates the attribute with the given {@code name}. @@ -105,7 +123,7 @@ default String getFirstValue(String name) { Set nameSet(); /** - * Returns all attributes that can be written. + * Returns all the attributes with read-write permissions in a particular {@link UserProfileContext}. * * @return the attributes */ @@ -131,52 +149,23 @@ default String getFirstValue(String name) { boolean isRequired(String name); /** - * Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove - * the root attributes. + * Returns only the attributes that have read permissions in a particular {@link UserProfileContext}. * - * @param includeBuiltin if the root attributes should be included. - * @return the attributes with read/write permission. + * @return the attributes with read permission. */ - default Map> getReadable(boolean includeBuiltin) { - return getReadable().entrySet().stream().filter(entry -> { - if (includeBuiltin) { - return true; - } - if (isRootAttribute(entry.getKey())) { - if (UserModel.LOCALE.equals(entry.getKey()) && !entry.getValue().isEmpty()) { - // locale is different form of built-in attribute in the sense it is related to a - // specific feature (i18n) and does not have a top-level attribute in the user representation - // the locale should be available from the attribute map if not empty - return true; - } - return false; - } - return true; - }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } + Map> getReadable(); /** - * Returns only the attributes that have read/write permissions. + * Returns the attributes as a {@link Map} that are accessible to a particular {@link UserProfileContext}. * - * @return the attributes with read/write permission. + * @return a map with all the attributes */ - Map> getReadable(); + Map> toMap(); /** - * Returns whether the attribute with the given {@code name} is a root attribute. + * Returns a {@link Map} holding any unmanaged attribute. * - * @param name the attribute name - * @return + * @return a map with any unmanaged attribute */ - default boolean isRootAttribute(String name) { - return UserModel.USERNAME.equals(name) - || UserModel.EMAIL.equals(name) - || UserModel.FIRST_NAME.equals(name) - || UserModel.LAST_NAME.equals(name) - || UserModel.LOCALE.equals(name); - } - - Map> toMap(); - Map> getUnmanagedAttributes(); } diff --git a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java index 537f4ebea685..3b6e5317c0ad 100644 --- a/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java +++ b/server-spi/src/main/java/org/keycloak/userprofile/UserProfileContext.java @@ -21,7 +21,7 @@ /** *

This interface represents the different contexts from where user profiles are managed. The core contexts are already - * available here representing the different parts in Keycloak where user profiles are managed. + * available here representing the different areas in Keycloak where user profiles are managed. * *

The context is crucial to drive the conditions that should be respected when managing user profiles. It might be possible * to include in the future metadata about contexts. As well as support custom contexts. @@ -30,16 +30,39 @@ */ public enum UserProfileContext { + /** + * In this context, a user profile is managed by themselves during an authentication flow such as when updating the user profile. + */ UPDATE_PROFILE(true), + + /** + * In this context, a user profile is managed through the management interface such as the Admin API. + */ USER_API(false), + + /** + * In this context, a user profile is managed by themselves through the account console. + */ ACCOUNT(true), + + /** + * In this context, a user profile is managed by themselves when authenticating through a broker. + */ IDP_REVIEW(false), + + /** + * In this context, a user profile is managed by themselves when registering to a realm. + */ REGISTRATION(false), + + /** + * In this context, a user profile is managed by themselves when updating their email through an application initiated action. + */ UPDATE_EMAIL(false); - protected boolean resetEmailVerified; + private boolean resetEmailVerified; - private UserProfileContext(boolean resetEmailVerified){ + UserProfileContext(boolean resetEmailVerified){ this.resetEmailVerified = resetEmailVerified; } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java index 5dd627daa920..e528e27c7782 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/IdpReviewProfileAuthenticator.java @@ -166,7 +166,7 @@ public String getServiceAccountClientLink() { profile.update((attributeName, userModel, oldValue) -> { if (attributeName.equals(UserModel.EMAIL)) { context.getAuthenticationSession().setAuthNote(UPDATE_PROFILE_EMAIL_CHANGED, "true"); - event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL)).success(); + event.clone().event(EventType.UPDATE_EMAIL).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, profile.getAttributes().getFirst(UserModel.EMAIL)).success(); } }); } catch (ValidationException pve) { @@ -187,7 +187,7 @@ public String getServiceAccountClientLink() { logger.debugf("Profile updated successfully after first authentication with identity provider '%s' for broker user '%s'.", brokerContext.getIdpConfig().getAlias(), userCtx.getUsername()); - String newEmail = profile.getAttributes().getFirstValue(UserModel.EMAIL); + String newEmail = profile.getAttributes().getFirst(UserModel.EMAIL); event.detail(Details.UPDATED_EMAIL, newEmail); diff --git a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java index 21fd8c1601c6..cd40519f9296 100755 --- a/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java +++ b/services/src/main/java/org/keycloak/authentication/forms/RegistrationUserCreation.java @@ -39,6 +39,7 @@ import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.services.messages.Messages; import org.keycloak.services.validation.Validation; +import org.keycloak.userprofile.Attributes; import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.UserProfileProvider; import org.keycloak.userprofile.ValidationException; @@ -71,11 +72,11 @@ public void validate(ValidationContext context) { context.getEvent().detail(Details.REGISTER_METHOD, "form"); UserProfile profile = getOrCreateUserProfile(context, formData); - String email = profile.getAttributes().getFirstValue(UserModel.EMAIL); - - String username = profile.getAttributes().getFirstValue(UserModel.USERNAME); - String firstName = profile.getAttributes().getFirstValue(UserModel.FIRST_NAME); - String lastName = profile.getAttributes().getFirstValue(UserModel.LAST_NAME); + Attributes attributes = profile.getAttributes(); + String email = attributes.getFirst(UserModel.EMAIL); + String username = attributes.getFirst(UserModel.USERNAME); + String firstName = attributes.getFirst(UserModel.FIRST_NAME); + String lastName = attributes.getFirst(UserModel.LAST_NAME); context.getEvent().detail(Details.EMAIL, email); context.getEvent().detail(Details.USERNAME, username); @@ -92,7 +93,7 @@ public void validate(ValidationContext context) { List errors = Validation.getFormErrorsFromValidation(pve.getErrors()); if (pve.hasError(Messages.EMAIL_EXISTS, Messages.INVALID_EMAIL)) { - context.getEvent().detail(Details.EMAIL, profile.getAttributes().getFirstValue(UserModel.EMAIL)); + context.getEvent().detail(Details.EMAIL, attributes.getFirst(UserModel.EMAIL)); } if (pve.hasError(Messages.EMAIL_EXISTS)) { diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java index d136af06ed64..d2066af3f01d 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateEmail.java @@ -168,7 +168,7 @@ public static UserProfile validateEmailUpdate(KeycloakSession session, UserModel public static void updateEmailNow(EventBuilder event, UserModel user, UserProfile emailUpdateValidationResult) { String oldEmail = user.getEmail(); - String newEmail = emailUpdateValidationResult.getAttributes().getFirstValue(UserModel.EMAIL); + String newEmail = emailUpdateValidationResult.getAttributes().getFirst(UserModel.EMAIL); event.event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, newEmail); emailUpdateValidationResult.update(false, new EventAuditingAttributeChangeListener(emailUpdateValidationResult, event)); } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java index 43a416411bcd..f3a38985e799 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountRestService.java @@ -24,12 +24,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Properties; import java.util.Set; -import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -66,12 +63,9 @@ import org.keycloak.models.UserConsentModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.ModelToRepresentation; -import org.keycloak.provider.ConfiguredProvider; import org.keycloak.representations.account.ClientRepresentation; import org.keycloak.representations.account.ConsentRepresentation; import org.keycloak.representations.account.ConsentScopeRepresentation; -import org.keycloak.representations.idm.UserProfileAttributeMetadata; -import org.keycloak.representations.idm.UserProfileMetadata; import org.keycloak.representations.account.UserRepresentation; import org.keycloak.representations.idm.ErrorRepresentation; import org.keycloak.representations.idm.GroupRepresentation; @@ -80,7 +74,6 @@ import org.keycloak.services.managers.UserConsentManager; import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.account.resources.ResourcesService; -import org.keycloak.services.resources.admin.UserProfileResource; import org.keycloak.services.util.ResolveRelative; import org.keycloak.storage.ReadOnlyException; import org.keycloak.theme.Theme; @@ -92,8 +85,6 @@ import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.keycloak.userprofile.ValidationException; import org.keycloak.userprofile.ValidationException.Error; -import org.keycloak.utils.GroupUtils; -import org.keycloak.validate.Validators; /** * @author Stian Thorgersen @@ -142,38 +133,17 @@ public UserRepresentation account(final @QueryParam("userProfileMetadata") Boole auth.requireOneOf(AccountRoles.MANAGE_ACCOUNT, AccountRoles.VIEW_PROFILE); UserModel user = auth.getUser(); - - UserRepresentation rep = new UserRepresentation(); - rep.setId(user.getId()); - UserProfileProvider provider = session.getProvider(UserProfileProvider.class); UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user); + UserRepresentation rep = profile.toRepresentation(); - rep.setAttributes(profile.getAttributes().getReadable(false)); - - addReadableBuiltinAttributes(user, rep, profile.getAttributes().getReadable(true).keySet()); + if (userProfileMetadata != null && !userProfileMetadata) { + rep.setUserProfileMetadata(null); + } - if(userProfileMetadata == null || userProfileMetadata.booleanValue()) - rep.setUserProfileMetadata(UserProfileResource.createUserProfileMetadata(session, profile)); - return rep; } - private void addReadableBuiltinAttributes(UserModel user, UserRepresentation rep, Set readableAttributes) { - setIfReadable(UserModel.USERNAME, readableAttributes, rep::setUsername, user::getUsername); - setIfReadable(UserModel.FIRST_NAME, readableAttributes, rep::setFirstName, user::getFirstName); - setIfReadable(UserModel.LAST_NAME, readableAttributes, rep::setLastName, user::getLastName); - setIfReadable(UserModel.EMAIL, readableAttributes, rep::setEmail, user::getEmail); - // emailVerified is readable when email is readable - setIfReadable(UserModel.EMAIL, readableAttributes, rep::setEmailVerified, user::isEmailVerified); - } - - private void setIfReadable(String attributeName, Set readableAttributes, Consumer setter, Supplier getter) { - if (readableAttributes.contains(attributeName)) { - setter.accept(getter.get()); - } - } - @Path("/") @POST @Consumes(MediaType.APPLICATION_JSON) @@ -185,7 +155,7 @@ public Response updateAccount(UserRepresentation rep) { event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name()); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); - UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.toAttributes(), auth.getUser()); + UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.getRawAttributes(), auth.getUser()); try { diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java index a5beb764e40d..7c86ba6abb85 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserProfileResource.java @@ -16,13 +16,9 @@ */ package org.keycloak.services.resources.admin; +import static org.keycloak.userprofile.UserProfileUtil.createUserProfileMetadata; + import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Function; -import java.util.stream.Collectors; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.GET; @@ -117,56 +113,4 @@ public Response update( return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build(); } - - public static UserProfileMetadata createUserProfileMetadata(KeycloakSession session, UserProfile profile) { - Attributes profileAttributes = profile.getAttributes(); - Map> am = profileAttributes.getReadable(); - - if(am == null) - return null; - Map> unmanagedAttributes = profileAttributes.getUnmanagedAttributes(); - - List attributes = am.keySet().stream() - .map(profileAttributes::getMetadata) - .filter(Objects::nonNull) - .filter(attributeMetadata -> !unmanagedAttributes.containsKey(attributeMetadata.getName())) - .sorted(Comparator.comparingInt(AttributeMetadata::getGuiOrder)) - .map(sam -> toRestMetadata(sam, session, profile)) - .collect(Collectors.toList()); - - UserProfileProvider provider = session.getProvider(UserProfileProvider.class); - UPConfig config = provider.getConfiguration(); - - List groups = config.getGroups().stream().map(new Function() { - @Override - public UserProfileAttributeGroupMetadata apply(UPGroup upGroup) { - return new UserProfileAttributeGroupMetadata(upGroup.getName(), upGroup.getDisplayHeader(), upGroup.getDisplayDescription(), upGroup.getAnnotations()); - } - }).collect(Collectors.toList()); - - return new UserProfileMetadata(attributes, groups); - } - - private static UserProfileAttributeMetadata toRestMetadata(AttributeMetadata am, KeycloakSession session, UserProfile profile) { - String group = null; - - if (am.getAttributeGroupMetadata() != null) { - group = am.getAttributeGroupMetadata().getName(); - } - - return new UserProfileAttributeMetadata(am.getName(), - am.getAttributeDisplayName(), - profile.getAttributes().isRequired(am.getName()), - profile.getAttributes().isReadOnly(am.getName()), - group, - am.getAnnotations(), - toValidatorMetadata(am, session)); - } - - private static Map> toValidatorMetadata(AttributeMetadata am, KeycloakSession session){ - // we return only validators which are instance of ConfiguredProvider. Others are expected as internal. - return am.getValidators() == null ? null : am.getValidators().stream() - .filter(avm -> (Validators.validator(session, avm.getValidatorId()) instanceof ConfiguredProvider)) - .collect(Collectors.toMap(AttributeValidatorMetadata::getValidatorId, AttributeValidatorMetadata::getValidatorConfig)); - } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java index 7d21d4eb4a90..70e1479aee2c 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java @@ -123,7 +123,6 @@ import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID; import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME; -import static org.keycloak.services.resources.admin.UserProfileResource.createUserProfileMetadata; import static org.keycloak.userprofile.UserProfileContext.USER_API; /** @@ -184,7 +183,7 @@ public Response updateUser(final UserRepresentation rep) { wasPermanentlyLockedOut = session.getProvider(BruteForceProtector.class).isPermanentlyLockedOut(session, realm, user); } - Map> attributes = new HashMap<>(rep.toAttributes()); + Map> attributes = new HashMap<>(rep.getRawAttributes()); if (rep.getAttributes() == null) { // include existing attributes in case no attributes are set so that validation takes into account the existing @@ -302,7 +301,9 @@ public UserRepresentation getUser( ) { auth.users().requireView(user); - UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, user); + UserProfileProvider provider = session.getProvider(UserProfileProvider.class); + UserProfile profile = provider.create(USER_API, user); + UserRepresentation rep = profile.toRepresentation(); if (realm.isIdentityFederationEnabled()) { List reps = getFederatedIdentities(user).collect(Collectors.toList()); @@ -314,16 +315,8 @@ public UserRepresentation getUser( } rep.setAccess(auth.users().getAccess(user)); - UserProfileProvider provider = session.getProvider(UserProfileProvider.class); - UserProfile profile = provider.create(USER_API, user); - Map> readableAttributes = profile.getAttributes().getReadable(false); - - if (rep.getAttributes() != null) { - rep.setAttributes(readableAttributes); - } - - if (userProfileMetadata) { - rep.setUserProfileMetadata(createUserProfileMetadata(session, profile)); + if (!userProfileMetadata) { + rep.setUserProfileMetadata(null); } return rep; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 55aded752a4d..6b180ac86565 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -154,7 +154,7 @@ public Response createUser(final UserRepresentation rep) { UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); - UserProfile profile = profileProvider.create(USER_API, rep.toAttributes()); + UserProfile profile = profileProvider.create(USER_API, rep.getRawAttributes()); try { Response response = UserResource.validateUserProfile(profile, session, auth.adminAuth()); diff --git a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java index 8da699d60c2a..f96417691c99 100644 --- a/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java +++ b/services/src/main/java/org/keycloak/userprofile/DeclarativeUserProfileProvider.java @@ -117,6 +117,7 @@ protected Attributes createAttributes(UserProfileContext context, Map } return new DefaultAttributes(context, attributes, user, metadata, session); } + return new LegacyAttributes(context, attributes, user, metadata, session); } @@ -153,11 +154,11 @@ private Function createUserFactory() { @Override public UserModel apply(Attributes attributes) { if (user == null) { - String userName = attributes.getFirstValue(UserModel.USERNAME); + String userName = attributes.getFirst(UserModel.USERNAME); // fallback to email in case email is allowed if (userName == null) { - userName = attributes.getFirstValue(UserModel.EMAIL); + userName = attributes.getFirst(UserModel.EMAIL); } user = session.users().addUser(session.getContext().getRealm(), userName); diff --git a/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java index d76d5575eb9a..a753ccb5c8e8 100644 --- a/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java +++ b/services/src/main/java/org/keycloak/userprofile/validator/ImmutableAttributeValidator.java @@ -75,7 +75,7 @@ public ValidationContext validate(Object input, String inputHint, ValidationCont return context; } - List email = attributeContext.getAttributes().getValues(UserModel.EMAIL); + List email = attributeContext.getAttributes().get(UserModel.EMAIL); if (UserModel.USERNAME.equals(attributeName) && collectionEquals(values, email)) { return context; diff --git a/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java b/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java index f06da402d3f6..4843484877fb 100644 --- a/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java +++ b/services/src/main/java/org/keycloak/userprofile/validator/UsernameMutationValidator.java @@ -70,7 +70,7 @@ public ValidationContext validate(Object input, String inputHint, ValidationCont if (!realm.isEditUsernameAllowed() && user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME))) { Attributes attributes = attributeContext.getAttributes(); - if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirstValue(UserModel.EMAIL))) { + if (realm.isRegistrationEmailAsUsername() && value.equals(attributes.getFirst(UserModel.EMAIL))) { // if username changed is because email as username is allowed so no validation should happen for update profile // it is expected that username changes when attributes are normalized by the provider return context; diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ModelTestExecutor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ModelTestExecutor.java index 025fa88ead15..281929630628 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ModelTestExecutor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ModelTestExecutor.java @@ -49,23 +49,18 @@ public void execute(LocalExecutionEvent event) throws Exception { super.execute(event); } else { TestResult result = new TestResult(); - if (annotation.skipForMapStorage()) { - result = TestResult.skipped(); - } - else { - try { - // Model test - wrap the call inside the - TestContext ctx = testContext.get(); - KeycloakTestingClient testingClient = ctx.getTestingClient(); - testingClient.server().runModelTest(testMethod.getDeclaringClass().getName(), testMethod.getName()); + try { + // Model test - wrap the call inside the + TestContext ctx = testContext.get(); + KeycloakTestingClient testingClient = ctx.getTestingClient(); + testingClient.server().runModelTest(testMethod.getDeclaringClass().getName(), testMethod.getName()); - result.setStatus(TestResult.Status.PASSED); - } catch (Throwable e) { - result.setStatus(TestResult.Status.FAILED); - result.setThrowable(e); - } finally { - result.setEnd(System.currentTimeMillis()); - } + result.setStatus(TestResult.Status.PASSED); + } catch (Throwable e) { + result.setStatus(TestResult.Status.FAILED); + result.setThrowable(e); + } finally { + result.setEnd(System.currentTimeMillis()); } // Need to use reflection this way... diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/ModelTest.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/ModelTest.java index 5eae2f7c25fd..673c98ae9c7e 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/ModelTest.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/ModelTest.java @@ -33,6 +33,4 @@ @Retention(RUNTIME) @Target({ElementType.METHOD}) // TODO: Maybe ElementClass.TYPE too? That way it will be possible to add the annotation on the the test class and not need to add on all the test methods inside the class public @interface ModelTest { - - boolean skipForMapStorage() default false; } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index 8ce563540f1f..6e44dc7a32c5 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -194,12 +194,10 @@ protected List getArgs(Map env) { commands.removeIf("--optimized"::equals); commands.add("--http-relative-path=/auth"); - if (!storeProvider.isMapStore()) { - if ("local".equals(cacheMode)) { - commands.add("--cache=local"); - } else { - commands.add("--cache-config-file=cluster-" + cacheMode + ".xml"); - } + if ("local".equals(cacheMode)) { + commands.add("--cache=local"); + } else { + commands.add("--cache-config-file=cluster-" + cacheMode + ".xml"); } if (configuration.getFipsMode() != FipsMode.DISABLED) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java index 40a2bf87d05d..19bff04d82a4 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakContainerEventsController.java @@ -52,7 +52,6 @@ import java.io.File; import java.io.IOException; -import java.util.Arrays; import org.jboss.shrinkwrap.api.Archive; @@ -139,10 +138,6 @@ protected void beforeNewContainerStart(RestartContainer restartContainer) { if (restartContainer.withoutKeycloakAddUserFile()) { removeKeycloakAddUserFile(); } - - if (restartContainer.initializeDatabase()) { - clearMapStorageFiles(); - } } /** @@ -202,17 +197,6 @@ private void deployAndDropAllTables(RestartContainer restartContainer) { } - private void clearMapStorageFiles() { - String filePath = System.getProperty("project.build.directory", "target/map"); - - File f = new File(filePath); - if (!f.exists()) return; - - Arrays.stream(f.listFiles()) - .filter(file -> file.getName().startsWith("map-") && file.getName().endsWith(".json")) - .forEach(File::delete); - } - /** * Copy keycloak-add-user.json only if it is jboss container (has jbossHome property). */ diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java index 50025b3d534a..5338105b49ff 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/KeycloakQuarkusServerDeployableContainer.java @@ -147,10 +147,7 @@ private Process startContainer() throws IOException { builder.environment().put("JAVA_OPTS", javaOpts); } - final StoreProvider storeProvider = StoreProvider.getCurrentProvider(); - final boolean isJpaStore = storeProvider.equals(StoreProvider.JPA) || storeProvider.equals(StoreProvider.LEGACY); - - if (!isJpaStore) { + if (!StoreProvider.JPA.equals(StoreProvider.getCurrentProvider())) { builder.environment().put("KEYCLOAK_ADMIN", "admin"); builder.environment().put("KEYCLOAK_ADMIN_PASSWORD", "admin"); } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java index f6c0f6d67e7b..c5bbbb64a05f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/model/StoreProvider.java @@ -20,7 +20,6 @@ import org.keycloak.utils.StringUtil; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -29,38 +28,7 @@ * @author Martin Bartos */ public enum StoreProvider { - CHM("chm") { - @Override - public void addStoreOptions(List commands) { - commands.add("--storage=" + getAlias()); - } - }, - FILE("file") { - @Override - public void addStoreOptions(List commands) { - commands.add("--storage=" + getAlias()); - } - }, JPA("jpa") { - @Override - public void addStoreOptions(List commands) { - commands.add("--storage=" + getAlias()); - getDbVendor().ifPresent(vendor -> commands.add("--storage-jpa-db=" + vendor)); - commands.add("--db-url=" + System.getProperty("keycloak.map.storage.connectionsJpa.url")); - commands.add("--db-username=" + System.getProperty("keycloak.map.storage.connectionsJpa.user")); - commands.add("--db-password=" + System.getProperty("keycloak.map.storage.connectionsJpa.password")); - } - }, - HOTROD("hotrod") { - @Override - public void addStoreOptions(List commands) { - commands.add("--storage=" + getAlias()); - commands.add("--storage-hotrod-host=" + System.getProperty("keycloak.connectionsHotRod.host")); - commands.add("--storage-hotrod-username=" + System.getProperty("keycloak.connectionsHotRod.username", "admin")); - commands.add("--storage-hotrod-password=" + System.getProperty("keycloak.connectionsHotRod.password", "admin")); - } - }, - LEGACY("legacy") { @Override public void addStoreOptions(List commands) { getDbVendor().ifPresent(vendor -> commands.add("--db=" + vendor)); @@ -92,7 +60,6 @@ public void addStoreOptions(List commands) { } }; - public static final String AUTH_SERVER_QUARKUS_MAP_STORAGE_PROFILE = "auth.server.quarkus.mapStorage.profile.config"; public static final String DB_VENDOR_PROPERTY = "keycloak.storage.connections.vendor"; private final String alias; @@ -116,32 +83,11 @@ public String getAlias() { return alias; } - public boolean isLegacyStore() { - return this.equals(LEGACY); - } - - public boolean isMapStore() { - return !isLegacyStore() && !this.equals(DEFAULT); - } - public static Optional getDbVendor() { return Optional.ofNullable(System.getProperty(DB_VENDOR_PROPERTY)).filter(StringUtil::isNotBlank); } public static StoreProvider getCurrentProvider() { - return getProviderByAlias(System.getProperty(AUTH_SERVER_QUARKUS_MAP_STORAGE_PROFILE, "")); - } - - /** - * Get Store Provider by alias - * - * @param alias alias - * @return store provider, LEGACY when vendor is specified, otherwise DEFAULT - */ - public static StoreProvider getProviderByAlias(String alias) { - return Arrays.stream(StoreProvider.values()) - .filter(f -> f.getAlias().equals(alias)) - .findFirst() - .orElseGet(() -> getDbVendor().isEmpty() ? DEFAULT : LEGACY); + return getDbVendor().isEmpty() ? DEFAULT : JPA; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java index 6b53bd045363..6d74ea677aba 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java @@ -37,12 +37,9 @@ import org.keycloak.admin.client.resource.UsersResource; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.Time; -import org.keycloak.models.RealmProvider; -import org.keycloak.models.cache.CacheRealmProvider; import org.keycloak.models.cache.UserCache; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; -import org.keycloak.provider.Provider; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RequiredActionProviderRepresentation; @@ -743,39 +740,4 @@ protected static InputStream httpsAwareConfigurationStream(InputStream input) th } return in; } - - /** - * MapRealmProvider uses session.invalidate() instead of calling e.g. - * session.clients().removeClients(realm); for clients (where clients are being removed one by one) - * - * Therefore it doesn't call session.users().preRemove(realm, client) for each client. - * Due to that JpaUserFederatedStorageProvider.preRemove(realm, client) is not called. - * So there remains objects in the database in user federation related tables after realm removal. - * - * Same for roles etc. - * - * Legacy federated storage is NOT supposed to work with map storage, so this method - * returns true if realm provider is "jpa" to be able to skip particular tests. - */ - protected boolean isJpaRealmProvider() { - return keycloakUsingProviderWithId(RealmProvider.class, "jpa"); - } - - protected boolean keycloakUsingProviderWithId(Class providerClass, String requiredId) { - String providerId = testingClient.server() - .fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(providerClass).getId()); - return Objects.equals(providerId, "\"" + requiredId + "\""); - } - - protected boolean isRealmCacheEnabled() { - String realmCache = testingClient.server() - .fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(CacheRealmProvider.class)); - return Objects.nonNull(realmCache); - } - - protected boolean isUserCacheEnabled() { - String userCache = testingClient.server() - .fetchString(s -> s.getKeycloakSessionFactory().getProviderFactory(UserCache.class)); - return Objects.nonNull(userCache); - } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java index 642fc6171daf..68dc72f3225c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceReadOnlyAttributesTest.java @@ -19,6 +19,8 @@ package org.keycloak.testsuite.account; import java.io.IOException; +import java.util.Map; +import java.util.Optional; import jakarta.ws.rs.BadRequestException; @@ -99,7 +101,7 @@ private void testAccountUpdateAttributeExpectFailure(String attrName) throws IOE private void testAccountUpdateAttributeExpectFailure(String attrName, boolean deniedForAdminAsWell) throws IOException { // Attribute not yet supposed to be on the user UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); - assertThat(user.getAttributes().keySet(), not(contains(attrName))); + assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName))); // Assert not possible to add the attribute to the user user.singleAttribute(attrName, "foo"); @@ -147,7 +149,7 @@ private void testAccountUpdateAttributeExpectFailure(String attrName, boolean de private void testAccountUpdateAttributeExpectSuccess(String attrName) throws IOException { // Attribute not yet supposed to be on the user UserRepresentation user = SimpleHttp.doGet(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).asJson(UserRepresentation.class); - assertThat(user.getAttributes().keySet(), not(contains(attrName))); + assertThat(Optional.ofNullable(user.getAttributes()).orElse(Map.of()).keySet(), not(contains(attrName))); // Assert not possible to add the attribute to the user user.singleAttribute(attrName, "foo"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java index f4cc39539ff2..5228ce3e779e 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceTest.java @@ -79,6 +79,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.is; @@ -278,7 +279,7 @@ public void testGetProfile() throws IOException { assertEquals("Brady", user.getLastName()); assertEquals("test-user@localhost", user.getEmail()); assertFalse(user.isEmailVerified()); - assertTrue(user.getAttributes().isEmpty()); + assertNull(user.getAttributes()); } @Test @@ -288,7 +289,7 @@ public void testUpdateSingleField() throws IOException { String originalFirstName = user.getFirstName(); String originalLastName = user.getLastName(); String originalEmail = user.getEmail(); - Map> originalAttributes = new HashMap<>(user.getAttributes()); + user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>())); try { RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); @@ -316,7 +317,6 @@ public void testUpdateSingleField() throws IOException { user.setFirstName(originalFirstName); user.setLastName(originalLastName); user.setEmail(originalEmail); - user.setAttributes(originalAttributes); SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse(); System.out.println(response.asString()); assertEquals(204, response.getStatus()); @@ -379,7 +379,8 @@ public void testUpdateProfileEvent() throws IOException { String originalFirstName = user.getFirstName(); String originalLastName = user.getLastName(); String originalEmail = user.getEmail(); - Map> originalAttributes = new HashMap<>(user.getAttributes()); + assertNull(user.getAttributes()); + user.setAttributes(new HashMap<>()); try { RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); @@ -417,7 +418,6 @@ public void testUpdateProfileEvent() throws IOException { user.setFirstName(originalFirstName); user.setLastName(originalLastName); user.setEmail(originalEmail); - user.setAttributes(originalAttributes); SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse(); System.out.println(response.asString()); assertEquals(204, response.getStatus()); @@ -431,7 +431,7 @@ public void testUpdateProfile() throws IOException { String originalFirstName = user.getFirstName(); String originalLastName = user.getLastName(); String originalEmail = user.getEmail(); - Map> originalAttributes = new HashMap<>(user.getAttributes()); + user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>())); try { RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); @@ -460,12 +460,7 @@ public void testUpdateProfile() throws IOException { user = updateAndGet(user); - if (isDeclarativeUserProfile()) { - assertEquals(2, user.getAttributes().size()); - assertTrue(user.getAttributes().get("attr1").isEmpty()); - } else { - assertEquals(1, user.getAttributes().size()); - } + assertEquals(1, user.getAttributes().size()); assertEquals(2, user.getAttributes().get("attr2").size()); assertThat(user.getAttributes().get("attr2"), containsInAnyOrder("val2", "val3")); @@ -522,7 +517,6 @@ public void testUpdateProfile() throws IOException { user.setFirstName(originalFirstName); user.setLastName(originalLastName); user.setEmail(originalEmail); - user.setAttributes(originalAttributes); SimpleHttp.Response response = SimpleHttp.doPost(getAccountUrl(null), httpClient).auth(tokenUtil.getToken()).json(user).asResponse(); System.out.println(response.asString()); assertEquals(204, response.getStatus()); @@ -556,6 +550,7 @@ public void testEmailReadableWhenEditUsernameDisabled() throws IOException { public void testUpdateProfileCannotChangeThroughAttributes() throws IOException { UserRepresentation user = getUser(); String originalUsername = user.getUsername(); + user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>())); Map> originalAttributes = new HashMap<>(user.getAttributes()); try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java index ba7d5bc8cf5d..39e8c0d5946d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountRestServiceWithUserProfileTest.java @@ -29,6 +29,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.junit.Before; import org.junit.Test; @@ -292,6 +293,7 @@ public void testUpdateProfileEventWithAdditionalAttributesAuditing() throws IOEx String originalFirstName = user.getFirstName(); String originalLastName = user.getLastName(); String originalEmail = user.getEmail(); + user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>())); Map> originalAttributes = new HashMap<>(user.getAttributes()); try { @@ -303,13 +305,13 @@ public void testUpdateProfileEventWithAdditionalAttributesAuditing() throws IOEx user.setEmail("bobby@localhost"); user.setFirstName("Homer"); user.setLastName("Simpsons"); - user.getAttributes().put("attr1", Collections.singletonList("val1")); - user.getAttributes().put("attr2", Collections.singletonList("val2")); + user.getAttributes().put("attr1", Collections.singletonList("val11")); + user.getAttributes().put("attr2", Collections.singletonList("val22")); + events.clear(); user = updateAndGet(user); //skip login to the REST API event - events.poll(); events.expectAccount(EventType.UPDATE_PROFILE).user(user.getId()) .detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name()) .detail(Details.PREVIOUS_EMAIL, originalEmail) @@ -318,7 +320,7 @@ public void testUpdateProfileEventWithAdditionalAttributesAuditing() throws IOEx .detail(Details.PREVIOUS_LAST_NAME, originalLastName) .detail(Details.UPDATED_FIRST_NAME, "Homer") .detail(Details.UPDATED_LAST_NAME, "Simpsons") - .detail(Details.PREF_UPDATED+"attr2", "val2") + .detail(Details.PREF_UPDATED+"attr2", "val22") .assertEvent(); events.assertEmpty(); @@ -379,22 +381,23 @@ public void testManageUserLocaleAttribute() throws IOException { realmRep.setInternationalizationEnabled(false); testRealm().update(realmRep); UserRepresentation user = getUser(); + user.setAttributes(Optional.ofNullable(user.getAttributes()).orElse(new HashMap<>())); try { user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR")); user = updateAndGet(user); - assertNull(user.getAttributes().get(UserModel.LOCALE)); + assertNull(user.getAttributes()); realmRep.setInternationalizationEnabled(true); testRealm().update(realmRep); - user.getAttributes().put(UserModel.LOCALE, List.of("pt_BR")); + user.singleAttribute(UserModel.LOCALE, "pt_BR"); user = updateAndGet(user); assertEquals("pt_BR", user.getAttributes().get(UserModel.LOCALE).get(0)); user.getAttributes().remove(UserModel.LOCALE); user = updateAndGet(user); - assertNull(user.getAttributes().get(UserModel.LOCALE)); + assertNull(user.getAttributes()); UserProfileMetadata metadata = user.getUserProfileMetadata(); @@ -406,7 +409,6 @@ public void testManageUserLocaleAttribute() throws IOException { } finally { realmRep.setInternationalizationEnabled(internationalizationEnabled); testRealm().update(realmRep); - user.getAttributes().remove(UserModel.LOCALE); updateAndGet(user); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java index c01783cfa8b5..6439718964d5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/DeclarativeUserTest.java @@ -37,6 +37,7 @@ import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -52,7 +53,6 @@ @EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE) public class DeclarativeUserTest extends AbstractAdminTest { - private static final String LOCALE_ATTR_KEY = "locale"; private static final String TEST_REALM_USER_MANAGER_NAME = "test-realm-user-manager"; private static final String REQUIRED_ATTR_KEY = "required-attr"; @@ -120,29 +120,6 @@ public void closeClient() { } } - @Test - public void testReturnAllConfiguredAttributesEvenIfNotSet() { - UserRepresentation user1 = new UserRepresentation(); - user1.setUsername("user1"); - user1.singleAttribute("attr1", "value1user1"); - user1.singleAttribute("attr2", "value2user1"); - String user1Id = createUser(user1); - - user1 = realm.users().get(user1Id).toRepresentation(); - Map> attributes = user1.getAttributes(); - assertEquals(4, attributes.size()); - List attr1 = attributes.get("attr1"); - assertEquals(1, attr1.size()); - assertEquals("value1user1", attr1.get(0)); - List attr2 = attributes.get("attr2"); - assertEquals(1, attr2.size()); - assertEquals("value2user1", attr2.get(0)); - List attrCustomA = attributes.get("custom-a"); - assertTrue(attrCustomA.isEmpty()); - assertTrue(attributes.containsKey("custom-a")); - assertTrue(attributes.containsKey("aName")); - } - @Test public void testDoNotReturnAttributeIfNotReadble() { UserRepresentation user1 = new UserRepresentation(); @@ -153,7 +130,7 @@ public void testDoNotReturnAttributeIfNotReadble() { user1 = realm.users().get(user1Id).toRepresentation(); Map> attributes = user1.getAttributes(); - assertEquals(4, attributes.size()); + assertEquals(2, attributes.size()); assertFalse(attributes.containsKey("custom-hidden")); setUserProfileConfiguration(this.realm, "{\"attributes\": [" @@ -170,8 +147,8 @@ public void testDoNotReturnAttributeIfNotReadble() { user1 = realm.users().get(user1Id).toRepresentation(); attributes = user1.getAttributes(); - assertEquals(5, attributes.size()); - assertTrue(attributes.containsKey("custom-hidden")); + assertEquals(2, attributes.size()); + assertFalse(attributes.containsKey("custom-hidden")); } @Test @@ -200,12 +177,17 @@ public void testUpdateUnsetAttributeWithEmptyValue() { UserResource userResource = realm.users().get(user1Id); user1 = userResource.toRepresentation(); - Map> attributes = user1.getAttributes(); - attributes.put("attr2", Collections.singletonList("")); + assertNull(user1.getAttributes()); + user1.singleAttribute("attr2", ""); // should be able to update the user when a read-only attribute has an empty or null value userResource.update(user1); - attributes.put("attr2", null); + user1 = userResource.toRepresentation(); + assertNull(user1.getAttributes()); + user1.setAttributes(new HashMap<>()); + user1.getAttributes().put("attr2", null); userResource.update(user1); + user1 = userResource.toRepresentation(); + assertNull(user1.getAttributes()); } @Test @@ -288,7 +270,7 @@ public void testUserLocale() { realm.update(realmRep); user1 = userResource.toRepresentation(); - assertNull(user1.getAttributes().get(UserModel.LOCALE)); + assertNull(user1.getAttributes()); } finally { realmRep.setInternationalizationEnabled(internationalizationEnabled); realm.update(realmRep); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java index eddb0c811dce..d6713529eddd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/PermissionsTest.java @@ -392,20 +392,9 @@ public void invoke(RealmResource realm, AtomicReference response) { } }, Resource.REALM, true); - if (isJpaRealmProvider()) { - // Caching is disabled with the new store, we need to skip these invocations - invoke(new Invocation() { - public void invoke(RealmResource realm) { - realm.clearRealmCache(); - } - }, Resource.REALM, true); - invoke(new Invocation() { - public void invoke(RealmResource realm) { - realm.clearUserCache(); - } - }, Resource.REALM, true); + invoke(RealmResource::clearRealmCache, Resource.REALM, true); + invoke(RealmResource::clearUserCache, Resource.REALM, true); - } // Delete realm invoke(new Invocation() { public void invoke(RealmResource realm) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java index 9329b1e409d9..2799d295640a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/ServerInfoTest.java @@ -88,11 +88,9 @@ public void testServerInfo() { assertNotNull(info.getSystemInfo().getServerTime()); assertNotNull(info.getSystemInfo().getUptime()); - if (isJpaRealmProvider()) { - Map jpaProviders = info.getProviders().get("connectionsJpa").getProviders(); - ProviderRepresentation jpaProvider = jpaProviders.values().iterator().next(); - log.infof("JPA Connections provider info: %s", jpaProvider.getOperationalInfo()); - } + Map jpaProviders = info.getProviders().get("connectionsJpa").getProviders(); + ProviderRepresentation jpaProvider = jpaProviders.values().iterator().next(); + log.infof("JPA Connections provider info: %s", jpaProvider.getOperationalInfo()); } @Override diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java index 4ac685b991bd..78a6e134fb0f 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserTest.java @@ -1529,20 +1529,12 @@ public void attributes() { String user2Id = createUser(user2); user1 = realm.users().get(user1Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(2, user1.getAttributes().size()); - } + assertEquals(2, user1.getAttributes().size()); assertAttributeValue("value1user1", user1.getAttributes().get("attr1")); assertAttributeValue("value2user1", user1.getAttributes().get("attr2")); user2 = realm.users().get(user2Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user2.getAttributes().size()); - } else { - assertEquals(2, user2.getAttributes().size()); - } + assertEquals(2, user2.getAttributes().size()); assertAttributeValue("value1user2", user2.getAttributes().get("attr1")); vals = user2.getAttributes().get("attr2"); assertEquals(2, vals.size()); @@ -1554,11 +1546,7 @@ public void attributes() { updateUser(realm.users().get(user1Id), user1); user1 = realm.users().get(user1Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(3, user1.getAttributes().size()); - } + assertEquals(3, user1.getAttributes().size()); assertAttributeValue("value3user1", user1.getAttributes().get("attr1")); assertAttributeValue("value2user1", user1.getAttributes().get("attr2")); assertAttributeValue("value4user1", user1.getAttributes().get("attr3")); @@ -1567,11 +1555,7 @@ public void attributes() { updateUser(realm.users().get(user1Id), user1); user1 = realm.users().get(user1Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(2, user1.getAttributes().size()); - } + assertEquals(2, user1.getAttributes().size()); assertAttributeValue("value2user1", user1.getAttributes().get("attr2")); assertAttributeValue("value4user1", user1.getAttributes().get("attr3")); @@ -1580,11 +1564,7 @@ public void attributes() { updateUser(realm.users().get(user1Id), user1); user1 = realm.users().get(user1Id).toRepresentation(); assertNotNull(user1.getAttributes()); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(2, user1.getAttributes().size()); - } + assertEquals(2, user1.getAttributes().size()); // empty attributes should remove attributes user1.setAttributes(Collections.emptyMap()); @@ -1602,21 +1582,13 @@ public void attributes() { realm.users().get(user1Id).update(user1); user1 = realm.users().get(user1Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(2, user1.getAttributes().size()); - } + assertEquals(2, user1.getAttributes().size()); user1.getAttributes().remove("foo"); realm.users().get(user1Id).update(user1); user1 = realm.users().get(user1Id).toRepresentation(); - if (isDeclarativeUserProfile()) { - assertEquals(managedAttributes.size(), user1.getAttributes().size()); - } else { - assertEquals(1, user1.getAttributes().size()); - } + assertEquals(1, user1.getAttributes().size()); } @Test @@ -1669,11 +1641,7 @@ public void updateUserWithReadOnlyAttributes() { user1 = realm.users().get(user1Id).toRepresentation(); assertEquals("foo", user1.getAttributes().get("usercertificate").get(0)); assertEquals("bar", user1.getAttributes().get("saml.persistent.name.id.for.foo").get(0)); - if (isDeclarativeUserProfile()) { - assertTrue(user1.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty()); - } else { - assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID)); - } + assertFalse(user1.getAttributes().containsKey(LDAPConstants.LDAP_ID)); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java index 4b5ba696a8ae..a5dfd0b26d50 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UsersTest.java @@ -99,50 +99,6 @@ public void searchUserMatchUsersCount() { assertThat(users.get(0).getUsername(), is("john.doe")); } - @Test - public void searchUserCaseSensitiveFirst() throws Exception { - Assume.assumeFalse(isJpaRealmProvider()); - Map attributes = new HashMap<>(); - attributes.put(Constants.REALM_ATTR_USERNAME_CASE_SENSITIVE, "true"); - try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm(REALM_NAME)) - .updateWith(r -> r.setAttributes(attributes)) - .update()) { - - createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); - - assertCaseSensitiveSearch(); - - RealmRepresentation realmRep = adminClient.realm(REALM_NAME).toRepresentation(); - RealmBuilder.edit(realmRep) - .attribute(Constants.REALM_ATTR_USERNAME_CASE_SENSITIVE, "false"); - realm.update(realmRep); - - assertCaseInsensitiveSearch(); - } - } - - @Test - public void searchUserCaseInSensitiveFirst() throws Exception { - Assume.assumeFalse(isJpaRealmProvider()); - Map attributes = new HashMap<>(); - attributes.put(Constants.REALM_ATTR_USERNAME_CASE_SENSITIVE, "false"); - try (AutoCloseable c = new RealmAttributeUpdater(adminClient.realm(REALM_NAME)) - .updateWith(r -> r.setAttributes(attributes)) - .update()) { - - createUser(REALM_NAME, "User", "password", "firstName", "lastName", "user@example.com"); - - assertCaseInsensitiveSearch(); - - RealmRepresentation realmRep = adminClient.realm(REALM_NAME).toRepresentation(); - RealmBuilder.edit(realmRep) - .attribute(Constants.REALM_ATTR_USERNAME_CASE_SENSITIVE, "true"); - realm.update(realmRep); - - assertCaseSensitiveSearch(); - } - } - /** * https://issues.redhat.com/browse/KEYCLOAK-15146 */ @@ -487,18 +443,4 @@ private void assertCaseInsensitiveSearch() { assertThat(realm.users().search("USER", true), hasSize(1)); assertThat(realm.users().search("Use", true), hasSize(0)); } - - private void assertCaseSensitiveSearch() { - // not-exact case-sensitive search - assertThat(realm.users().search("user"), hasSize(0)); - assertThat(realm.users().search("User"), hasSize(1)); - assertThat(realm.users().search("USER"), hasSize(0)); - assertThat(realm.users().search("Use"), hasSize(1)); - - // exact case-sensitive search - assertThat(realm.users().search("user", true), hasSize(0)); - assertThat(realm.users().search("User", true), hasSize(1)); - assertThat(realm.users().search("USER", true), hasSize(0)); - assertThat(realm.users().search("Use", true), hasSize(0)); - } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java index b8b126c15bc4..de39a6a78b25 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/group/GroupSearchTest.java @@ -118,8 +118,7 @@ public void querySearch() throws Exception { search(buildSearchQuery(ATTR_QUOTES_NAME_ESCAPED, ATTR_QUOTES_VAL_ESCAPED), GROUP3); // "filtered" attribute won't take effect when JPA is used - String[] expectedRes = isLegacyJpaStore() ? new String[]{GROUP1, GROUP2} : new String[]{GROUP2}; - search(buildSearchQuery(ATTR_URL_NAME, ATTR_URL_VAL, ATTR_FILTERED_NAME, ATTR_FILTERED_VAL), expectedRes); + search(buildSearchQuery(ATTR_URL_NAME, ATTR_URL_VAL, ATTR_FILTERED_NAME, ATTR_FILTERED_VAL), new String[]{GROUP1, GROUP2}); } finally { resetSearchableAttributes(); } @@ -287,10 +286,6 @@ private static String buildSearchQuery(String firstAttrName, String firstAttrVal return sb.toString(); } - private boolean isLegacyJpaStore() { - return keycloakUsingProviderWithId(GroupProvider.class, "jpa"); - } - @Override public void addTestRealms(List testRealms) { loadTestRealm(testRealmReps); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java index 23a6e367c0ad..2ca7ad53319d 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/realm/RealmTest.java @@ -21,7 +21,6 @@ import org.apache.commons.io.IOUtils; import org.hamcrest.CoreMatchers; import org.hamcrest.Matchers; -import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -251,10 +250,8 @@ public void excludesFieldsFromAttributes() { ); // This attribute is represented in Legacy store as attribute and for Map store as a field - if (!StoreProvider.getCurrentProvider().isMapStore()) { - expectedAttributes.add(OTPPolicy.REALM_REUSABLE_CODE_ATTRIBUTE); - expectedAttributesCount++; - } + expectedAttributes.add(OTPPolicy.REALM_REUSABLE_CODE_ATTRIBUTE); + expectedAttributesCount++; assertThat(attributesKeys.size(), CoreMatchers.is(expectedAttributesCount)); assertThat(attributesKeys, CoreMatchers.is(expectedAttributes)); @@ -738,7 +735,6 @@ public static void assertRealm(RealmRepresentation realm, RealmRepresentation st @Test public void clearRealmCache() { - Assume.assumeTrue("Realm cache disabled.", isRealmCacheEnabled()); RealmRepresentation realmRep = realm.toRepresentation(); assertTrue(testingClient.testing().cache("realms").contains(realmRep.getId())); @@ -750,7 +746,6 @@ public void clearRealmCache() { @Test public void clearUserCache() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); UserRepresentation user = new UserRepresentation(); user.setUsername("clearcacheuser"); Response response = realm.users().create(user); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java index 0df1b33ad6c9..af89de219444 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AccountLinkTest.java @@ -19,14 +19,12 @@ import java.util.Collections; import jakarta.ws.rs.core.Response; import org.jboss.arquillian.graphene.page.Page; -import org.junit.Assume; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UsersResource; -import org.keycloak.common.Profile; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -39,7 +37,6 @@ import org.keycloak.storage.UserStorageProvider; import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.Assert; -import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProvider; import org.keycloak.testsuite.federation.PassThroughFederatedUserStorageProviderFactory; @@ -183,7 +180,6 @@ public void testDeleteFederatedUserFederatedIdentityOnProviderRemoval() { BrokerTestTools.createKcOidcBroker(adminClient, CHILD_IDP, testIdpToDelete); // Create user federation - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); ComponentRepresentation memProvider = new ComponentRepresentation(); memProvider.setName("memory"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java index 7cbd4ac9f9ab..3262c23f5d5c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/AdminEventStoreProviderTest.java @@ -224,7 +224,6 @@ public void clearOld() { @Test public void expireOld() { - Assume.assumeTrue("Map storage event store provider does not support changing expiration of existing events", keycloakUsingProviderWithId(EventStoreProvider.class, "jpa")); testing().onAdminEvent(create(System.currentTimeMillis() - 30000, realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); testing().onAdminEvent(create(System.currentTimeMillis() - 20000, realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); testing().onAdminEvent(create(System.currentTimeMillis(), realmId, OperationType.CREATE, realmId, "clientId", "userId", "127.0.0.1", "/admin/realms/master", "error"), false); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java index e726b8b4c0eb..9480c9654891 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/events/EventStoreProviderTest.java @@ -260,7 +260,6 @@ public void maxLengthWithNull(){ @Test public void clearOld() { - Assume.assumeTrue("Map storage event store provider does not support changing expiration of existing events", keycloakUsingProviderWithId(EventStoreProvider.class, "jpa")); testing().onEvent(create(System.currentTimeMillis() - 300000, EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); testing().onEvent(create(System.currentTimeMillis() - 200000, EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); testing().onEvent(create(System.currentTimeMillis(), EventType.LOGIN, realmId, "clientId", "userId", "127.0.0.1", "error")); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java index 00779ebd38cc..e24648ffbd55 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPAdminRestApiWithUserProfileTest.java @@ -18,6 +18,8 @@ package org.keycloak.testsuite.federation.ldap; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.keycloak.testsuite.forms.VerifyProfileTest.disableDynamicUserProfile; @@ -71,14 +73,16 @@ public void testUpdateReadOnlyAttributeWhenNotSetToUser() throws Exception { UserResource user = testRealm().users().get(newUserId); UserRepresentation userRep = user.toRepresentation(); - - assertTrue(userRep.getAttributes().containsKey(LDAPConstants.LDAP_ID)); - assertTrue(userRep.getAttributes().get(LDAPConstants.LDAP_ID).isEmpty()); + assertNull(userRep.getAttributes()); userRep.singleAttribute(LDAPConstants.LDAP_ID, ""); user.update(userRep); + userRep = testRealm().users().get(newUserId).toRepresentation(); + assertNull(userRep.getAttributes()); userRep.singleAttribute(LDAPConstants.LDAP_ID, null); user.update(userRep); + userRep = testRealm().users().get(newUserId).toRepresentation(); + assertNull(userRep.getAttributes()); try { userRep.singleAttribute(LDAPConstants.LDAP_ID, "should-fail"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java index c3d32acb634a..5a64ea697c49 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPMultipleAttributesTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.federation.ldap; import org.junit.Assert; -import org.junit.Assume; import org.junit.ClassRule; import org.junit.FixMethodOrder; import org.junit.Test; @@ -106,7 +105,6 @@ protected void afterImportTestRealm() { @Test public void testUserImport() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); UserStorageUtil.userCache(session).clear(); @@ -122,7 +120,6 @@ public void testUserImport() { @Test public void testModel() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); UserStorageUtil.userCache(session).clear(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java index cdb98537077d..80715c40e448 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPProvidersIntegrationTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.federation.ldap; import org.junit.Assert; -import org.junit.Assume; import org.junit.ClassRule; import org.junit.FixMethodOrder; import org.junit.Test; @@ -755,7 +754,6 @@ public void testCommaInUsername() { @Test public void testHardcodedAttributeMapperTest() throws Exception { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); // Create hardcoded mapper for "description" testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); @@ -1043,7 +1041,6 @@ public void testSearch() { @Test public void testSearchWithCustomLDAPFilter() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); // Add custom filter for searching users testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); @@ -1242,7 +1239,6 @@ public void testSearchByAttributes() { // KEYCLOAK-9002 @Test public void testSearchWithPartiallyCachedUser() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { UserStorageUtil.userCache(session).clear(); }); @@ -1269,7 +1265,6 @@ public void testSearchWithPartiallyCachedUser() { @Test public void testLDAPUserRefreshCache() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { UserStorageUtil.userCache(session).clear(); }); @@ -1313,7 +1308,6 @@ public void testLDAPUserRefreshCache() { @Test public void testCacheUser() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); String userId = testingClient.server().fetch(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); ctx.getLdapModel().setCachePolicy(UserStorageProviderModel.CachePolicy.NO_CACHE); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java index 530f9991ad39..6afc7529bba3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPRoleMappingsTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.federation.ldap; import org.junit.Assert; -import org.junit.Assume; import org.junit.ClassRule; import org.junit.FixMethodOrder; import org.junit.Test; @@ -334,7 +333,6 @@ private static void deleteRoleMappingsInLDAP(RoleLDAPStorageMapper roleMapper, L */ @Test public void test04_syncRoleMappings() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); RealmModel appRealm = ctx.getRealm(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPMultipleAttributesNoImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPMultipleAttributesNoImportTest.java index f6b7b9e14d6c..8c11ca5765ad 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPMultipleAttributesNoImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPMultipleAttributesNoImportTest.java @@ -18,7 +18,6 @@ package org.keycloak.testsuite.federation.ldap.noimport; import org.junit.Assert; -import org.junit.Assume; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; @@ -45,7 +44,6 @@ protected boolean isImportEnabled() { @Test public void testUserImport() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); testingClient.server().run(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); UserStorageUtil.userCache(session).clear(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPProvidersIntegrationNoImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPProvidersIntegrationNoImportTest.java index f9a0010369e0..ec8594131b00 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPProvidersIntegrationNoImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPProvidersIntegrationNoImportTest.java @@ -26,7 +26,6 @@ import java.util.Map; import org.junit.Assert; -import org.junit.Assume; import org.junit.FixMethodOrder; import org.junit.Ignore; import org.junit.Test; @@ -212,9 +211,6 @@ public void zzTestUnlinkUsers() { @Test public void testFullNameMapperWriteOnly() { - Assume.assumeTrue("User cache disabled. UserModel behaves differently when it's cached adapter and when not. See https://github.com/keycloak/keycloak/discussions/10004", - isUserCacheEnabled()); - ComponentRepresentation firstNameMapperRep = testingClient.server().fetch(session -> { LDAPTestContext ctx = LDAPTestContext.init(session); RealmModel appRealm = ctx.getRealm(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPRoleMappingsNoImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPRoleMappingsNoImportTest.java index 652cf5779558..3749f72d099f 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPRoleMappingsNoImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/noimport/LDAPRoleMappingsNoImportTest.java @@ -18,8 +18,6 @@ package org.keycloak.testsuite.federation.ldap.noimport; import org.junit.Assert; -import org.junit.Assume; -import org.junit.Before; import org.junit.ClassRule; import org.junit.FixMethodOrder; import org.junit.Test; @@ -62,11 +60,6 @@ protected LDAPRule getLDAPRule() { return ldapRule; } - @Before - public void enabled() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - } - @Override protected boolean isImportEnabled() { return false; diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java index dc6f5d429f0f..bf028b5b3b14 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/ClientStorageTest.java @@ -19,7 +19,6 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -325,8 +324,6 @@ private void testDirectGrant(String clientId) { @Test public void testDailyEviction() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - testIsCached(); testingClient.server().run(session -> { @@ -348,10 +345,9 @@ public void testDailyEviction() { testIsCached(); } + @Test public void testWeeklyEviction() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - testIsCached(); testingClient.server().run(session -> { @@ -376,10 +372,9 @@ public void testWeeklyEviction() { testIsCached(); } + @Test public void testMaxLifespan() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - testIsCached(); testingClient.server().run(session -> { @@ -417,8 +412,6 @@ private void testNotCached() { @Test public void testIsCached() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - testingClient.server().run(session -> { RealmModel realm = session.realms().getRealmByName("test"); ClientModel hardcoded = realm.getClientByClientId("hardcoded-client"); @@ -430,8 +423,6 @@ public void testIsCached() { @Test public void testNoCache() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - testIsCached(); testingClient.server().run(session -> { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java index 449c27c17b0c..0a57b4003e9b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/FederatedStorageExportImportTest.java @@ -59,8 +59,6 @@ public class FederatedStorageExportImportTest extends AbstractAuthTest { @Before public void setDirs() { - Assume.assumeTrue("RealmProvider is not 'jpa'", isJpaRealmProvider()); - File baseDir = new File(System.getProperty("auth.server.config.dir", "target")); exportFileAbsolutePath = new File (baseDir, "singleFile-full.json").getAbsolutePath(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java index c3a30af058f8..2ca8868f74d8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageFailureTest.java @@ -20,7 +20,6 @@ import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -92,8 +91,6 @@ public void configureTestRealm(RealmRepresentation testRealm) { @Before public void addProvidersBeforeTest() { - Assume.assumeTrue("RealmProvider is not 'jpa'", isJpaRealmProvider()); - ComponentRepresentation memProvider = new ComponentRepresentation(); memProvider.setName("failure"); memProvider.setProviderId(FailableHardcodedStorageProviderFactory.PROVIDER_ID); @@ -152,8 +149,6 @@ private String addComponent(ComponentRepresentation component) { */ @Test public void testKeycloak5350() throws Exception { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - oauth.scope(OAuth2Constants.OFFLINE_ACCESS); oauth.clientId("offline-client"); oauth.redirectUri(OAuthClient.AUTH_SERVER_ROOT + "/offline-client"); @@ -254,8 +249,6 @@ private void loginSuccessAndLogout(String username, String password) { @Test public void testKeycloak5926() { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - oauth.clientId("test-app"); oauth.redirectUri(OAuthClient.APP_AUTH_ROOT); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java index eafb244f8192..02305fe6db6c 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageOTPTest.java @@ -91,8 +91,6 @@ public void configureTestRealm(RealmRepresentation testRealm) { @Before public void addProvidersBeforeTest() throws URISyntaxException, IOException { - Assume.assumeTrue("RealmProvider is not 'jpa'", isJpaRealmProvider()); - ComponentRepresentation dummyProvider = new ComponentRepresentation(); dummyProvider.setName("dummy"); dummyProvider.setId(componentId); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java index 5c9a73d4e39c..87bb3752b5fe 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/storage/UserStorageTest.java @@ -6,7 +6,6 @@ import org.jboss.arquillian.graphene.page.Page; import org.junit.After; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; @@ -126,8 +125,6 @@ public class UserStorageTest extends AbstractAuthTest { @Before public void addProvidersBeforeTest() throws URISyntaxException, IOException { - Assume.assumeTrue("User cache disabled.", isUserCacheEnabled()); - ComponentRepresentation memProvider = new ComponentRepresentation(); memProvider.setName("memory"); memProvider.setProviderId(UserMapStorageFactory.PROVIDER_ID); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java index 1d221a19f15c..9eaa2d9248e5 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/sync/SyncFederationTest.java @@ -38,8 +38,6 @@ import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; -import org.junit.Assume; -import org.junit.Before; /** * Test with Dummy providers @@ -51,11 +49,6 @@ public class SyncFederationTest extends AbstractAuthTest { private static final Logger log = Logger.getLogger(SyncFederationTest.class); - @Before - public void enabled() { - Assume.assumeTrue("RealmProvider is not 'jpa'", isJpaRealmProvider()); - } - /** * Test that period sync is triggered when creating a synchronized User Storage Provider * diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java index c87ff7692d14..fb1a674a8ea3 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/RegisterTest.java @@ -188,8 +188,6 @@ public void registerUpperCaseEmailAsUsername() throws IOException { @Test public void registerUpperCaseEmailWithChangedEmailAsUsername() throws IOException { - Assume.assumeTrue("See https://github.com/keycloak/keycloak/issues/10245", isUserCacheEnabled()); - String userId = registerUpperCaseAndGetUserId(false); assertThat(userId, notNullValue()); oauth.openLogout(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/BadRealmTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/BadRealmTest.java index ed5862ec8295..76b1f963b471 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/BadRealmTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/BadRealmTest.java @@ -31,7 +31,7 @@ public void testBadRealmName(KeycloakSession session) { } @Test - @ModelTest(skipForMapStorage = true) // when map storage is enabled, the id is always converted into a valid UUID. + @ModelTest public void testBadRealmId(KeycloakSession session) { RealmManager manager = new RealmManager(session); try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java index bfbb556bbc28..85daa6818369 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/CacheTest.java @@ -17,7 +17,6 @@ package org.keycloak.testsuite.model; -import org.junit.Assume; import org.junit.Test; import org.keycloak.models.ClientModel; import org.keycloak.models.RealmModel; @@ -44,62 +43,44 @@ */ public class CacheTest extends AbstractTestRealmKeycloakTest { - private ClientModel testApp = null; - private int grantedRolesCount=0; - private RealmModel realm = null; - private UserModel user = null; - - @Override - public void configureTestRealm(RealmRepresentation testRealm) { - } - - @Test - public void testStaleCache() throws Exception { - Assume.assumeTrue("Realm cache disabled.", isRealmCacheEnabled()); - testingClient.server().run(session -> { - String appId = null; - { - // load up cache - - RealmModel realm = session.realms().getRealmByName("test"); - assertTrue(realm instanceof RealmAdapter); - ClientModel testApp = realm.getClientByClientId("test-app"); - assertTrue(testApp instanceof ClientAdapter); - assertNotNull(testApp); - appId = testApp.getId(); - assertTrue(testApp.isEnabled()); - - - - // update realm, then get an AppModel and change it. The AppModel would not be a cache adapter - - realm = session.realms().getRealmsStream().filter(r -> { - assertTrue(r instanceof RealmAdapter); - if ("test".equals(r.getName())) - return true; - return false; - }).findFirst().orElse(null); - - assertNotNull(realm); - - realm.setAccessCodeLifespanLogin(200); - testApp = realm.getClientByClientId("test-app"); - - assertNotNull(testApp); - testApp.setEnabled(false); - - - // make sure that app cache was flushed and enabled changed - - - realm = session.realms().getRealmByName("test"); - Assert.assertEquals(200, realm.getAccessCodeLifespanLogin()); - testApp = session.clients().getClientById(realm, appId); - Assert.assertFalse(testApp.isEnabled()); - - } - }); - } + @Override + public void configureTestRealm(RealmRepresentation testRealm) { + } + + @Test + public void testStaleCache() throws Exception { + testingClient.server().run(session -> { + // load up cache + RealmModel realm = session.realms().getRealmByName("test"); + assertTrue(realm instanceof RealmAdapter); + ClientModel testApp = realm.getClientByClientId("test-app"); + assertTrue(testApp instanceof ClientAdapter); + assertNotNull(testApp); + String appId = testApp.getId(); + assertTrue(testApp.isEnabled()); + + // update realm, then get an AppModel and change it. The AppModel would not be a cache adapter + realm = session.realms().getRealmsStream().filter(r -> { + assertTrue(r instanceof RealmAdapter); + return "test".equals(r.getName()); + }).findFirst().orElse(null); + + assertNotNull(realm); + + realm.setAccessCodeLifespanLogin(200); + testApp = realm.getClientByClientId("test-app"); + + assertNotNull(testApp); + testApp.setEnabled(false); + + // make sure that app cache was flushed and enabled changed + realm = session.realms().getRealmByName("test"); + Assert.assertEquals(200, realm.getAccessCodeLifespanLogin()); + testApp = session.clients().getClientById(realm, appId); + Assert.assertFalse(testApp.isEnabled()); + }); + } + @Test public void testAddUserNotAddedToCache() { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java index 3cfaed49bb82..7cdfdab474cb 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/ConcurrentTransactionsTest.java @@ -194,7 +194,7 @@ public void persistClient(KeycloakSession session) { // KEYCLOAK-3296 , KEYCLOAK-3494 @Test - @ModelTest(skipForMapStorage = true) // skipped for map storage - to be revisited (GHI #12910) + @ModelTest public void removeUserAttribute(KeycloakSession session) throws Exception { try { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java index 2f76b6791016..f707d7479830 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/model/UserConsentWithUserStorageModelTest.java @@ -19,7 +19,6 @@ import org.junit.After; import org.junit.Assert; -import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.keycloak.component.ComponentModel; @@ -58,7 +57,6 @@ public class UserConsentWithUserStorageModelTest extends AbstractTestRealmKeyclo @Before public void before() { - Assume.assumeTrue("RealmProvider is not 'jpa'", isJpaRealmProvider()); testingClient.server().run(UserConsentWithUserStorageModelTest::setupEnv); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java index fb56277ae6d6..7d38e5edba9d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/user/profile/UserProfileTest.java @@ -55,6 +55,7 @@ import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.AbstractUserRepresentation; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; @@ -114,6 +115,7 @@ public void testReadOnlyAllowed() throws Exception { // create a user with attribute foo value 123 allowed by the profile now but disallowed later UPConfig config = parseDefaultConfig(); config.addOrReplaceAttribute(new UPAttribute("foo", new UPAttributePermissions(Set.of(), Set.of(ROLE_ADMIN)))); + config.getAttribute(UserModel.EMAIL).setPermissions(new UPAttributePermissions(Set.of(ROLE_USER), Set.of(ROLE_ADMIN))); RealmResource realmRes = testRealm(); realmRes.users().userProfile().update(config); @@ -142,6 +144,18 @@ public void testReadOnlyAllowed() throws Exception { profile.validate(); }); + // it should work if foo is read-only in the context + getTestingClient().server(TEST_REALM_NAME).run(session -> { + RealmModel realm = session.getContext().getRealm(); + UserModel user = session.users().getUserById(realm, userId); + user.setEmail(null); + UserProfileProvider provider = getUserProfileProvider(session); + Map attributes = new HashMap<>(user.getAttributes()); + attributes.put("email", ""); + UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes, user); + profile.validate(); + }); + // it should fail if foo can be modified getTestingClient().server(TEST_REALM_NAME).run(session -> { RealmModel realm = session.getContext().getRealm(); @@ -174,7 +188,7 @@ private static void testIdempotentProfile(KeycloakSession session) { // once created, profile attributes can not be changed assertTrue(profile.getAttributes().contains(UserModel.USERNAME)); - assertNull(profile.getAttributes().getFirstValue(UserModel.USERNAME)); + assertNull(profile.getAttributes().getFirst(UserModel.USERNAME)); } @Test @@ -413,11 +427,11 @@ private static void testGetProfileAttributes(KeycloakSession session) throws IOE assertTrue(ve.isAttributeOnError("address")); } - assertNotNull(attributes.getFirstValue(UserModel.USERNAME)); - assertNotNull(attributes.getFirstValue(UserModel.EMAIL)); - assertNotNull(attributes.getFirstValue(UserModel.FIRST_NAME)); - assertNotNull(attributes.getFirstValue(UserModel.LAST_NAME)); - assertNull(attributes.getFirstValue("address")); + assertNotNull(attributes.getFirst(UserModel.USERNAME)); + assertNotNull(attributes.getFirst(UserModel.EMAIL)); + assertNotNull(attributes.getFirst(UserModel.FIRST_NAME)); + assertNotNull(attributes.getFirst(UserModel.LAST_NAME)); + assertNull(attributes.getFirst("address")); user.setAttribute("address", Arrays.asList("fixed-address")); @@ -426,7 +440,7 @@ private static void testGetProfileAttributes(KeycloakSession session) throws IOE profile.validate(); - assertNotNull(attributes.getFirstValue("address")); + assertNotNull(attributes.getFirst("address")); } @Test @@ -1516,20 +1530,20 @@ private static void testDoNotRemoveAttributes(KeycloakSession session) throws IO profile = provider.create(UserProfileContext.USER_API, user); Attributes userAttributes = profile.getAttributes(); - assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL)); - assertEquals("Test Value", userAttributes.getFirstValue("test-attribute")); - assertEquals("changed", userAttributes.getFirstValue("foo")); + assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL)); + assertEquals("Test Value", userAttributes.getFirst("test-attribute")); + assertEquals("changed", userAttributes.getFirst("foo")); attributes.remove("foo"); - attributes.put("test-attribute", userAttributes.getFirstValue("test-attribute")); + attributes.put("test-attribute", userAttributes.getFirst("test-attribute")); profile = provider.create(UserProfileContext.USER_API, attributes, user); profile.update(true); profile = provider.create(UserProfileContext.USER_API, user); userAttributes = profile.getAttributes(); // remove attribute if not set - assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL)); - assertEquals("Test Value", userAttributes.getFirstValue("test-attribute")); - assertNull(userAttributes.getFirstValue("foo")); + assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL)); + assertEquals("Test Value", userAttributes.getFirst("test-attribute")); + assertNull(userAttributes.getFirst("foo")); config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER)))); provider.setConfiguration(JsonSerialization.writeValueAsString(config)); @@ -1539,8 +1553,8 @@ private static void testDoNotRemoveAttributes(KeycloakSession session) throws IO profile = provider.create(UserProfileContext.USER_API, user); userAttributes = profile.getAttributes(); // do not remove test-attribute because admin does not have write permissions - assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL)); - assertEquals("Test Value", userAttributes.getFirstValue("test-attribute")); + assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL)); + assertEquals("Test Value", userAttributes.getFirst("test-attribute")); config.addOrReplaceAttribute(new UPAttribute("test-attribute", new UPAttributePermissions(Set.of(), Set.of(ROLE_USER, ROLE_ADMIN)))); provider.setConfiguration(JsonSerialization.writeValueAsString(config)); @@ -1550,8 +1564,8 @@ private static void testDoNotRemoveAttributes(KeycloakSession session) throws IO profile = provider.create(UserProfileContext.USER_API, user); userAttributes = profile.getAttributes(); // removes the test-attribute attribute because now admin has write permission - assertEquals("new-email@test.com", userAttributes.getFirstValue(UserModel.EMAIL)); - assertNull(userAttributes.getFirstValue("test-attribute")); + assertEquals("new-email@test.com", userAttributes.getFirst(UserModel.EMAIL)); + assertNull(userAttributes.getFirst("test-attribute")); } @Test @@ -1594,11 +1608,11 @@ private static void testRemoveEmptyRootAttribute(KeycloakSession session) throws } private static void assertRemoveEmptyRootAttribute(Map> attributes, UserModel user, Attributes upAttributes) { - assertNull(upAttributes.getFirstValue(UserModel.LAST_NAME)); + assertNull(upAttributes.getFirst(UserModel.LAST_NAME)); assertNull(user.getLastName()); - assertNull(upAttributes.getFirstValue(UserModel.EMAIL)); + assertNull(upAttributes.getFirst(UserModel.EMAIL)); assertNull(user.getEmail()); - assertEquals(upAttributes.getFirstValue(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0)); + assertEquals(upAttributes.getFirst(UserModel.FIRST_NAME), attributes.get(UserModel.FIRST_NAME).get(0)); } @Test @@ -1693,6 +1707,77 @@ private static void testUnmanagedPolicy(KeycloakSession session) throws IOExcept assertFalse(profile.getAttributes().isReadOnly("foo")); } + @Test + public void testOptionalRootAttributesAsUnmanagedAttribute() { + getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalRootAttributesAsUnmanagedAttribute); + } + + private static void testOptionalRootAttributesAsUnmanagedAttribute(KeycloakSession session) throws IOException { + UPConfig config = parseDefaultConfig(); + UserProfileProvider provider = getUserProfileProvider(session); + provider.setConfiguration(JsonSerialization.writeValueAsString(config)); + Map rawAttributes = new HashMap<>(); + rawAttributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org"); + rawAttributes.put(UserModel.EMAIL, org.keycloak.models.utils.KeycloakModelUtils.generateId() + "@keycloak.org"); + rawAttributes.put(UserModel.FIRST_NAME, "firstName"); + rawAttributes.put(UserModel.LAST_NAME, "lastName"); + UserProfile profile = provider.create(UserProfileContext.USER_API, rawAttributes); + UserModel user = profile.create(); + assertEquals(rawAttributes.get(UserModel.FIRST_NAME), user.getFirstName()); + assertEquals(rawAttributes.get(UserModel.LAST_NAME), user.getLastName()); + AbstractUserRepresentation rep = profile.toRepresentation(); + assertEquals(rawAttributes.get(UserModel.FIRST_NAME), rep.getFirstName()); + assertEquals(rawAttributes.get(UserModel.LAST_NAME), rep.getLastName()); + assertNull(rep.getAttributes()); + + config.removeAttribute(UserModel.FIRST_NAME); + config.removeAttribute(UserModel.LAST_NAME); + provider.setConfiguration(JsonSerialization.writeValueAsString(config)); + profile = provider.create(UserProfileContext.USER_API, user); + Attributes attributes = profile.getAttributes(); + assertNull(attributes.getFirst(UserModel.FIRST_NAME)); + assertNull(attributes.getFirst(UserModel.LAST_NAME)); + rep = profile.toRepresentation(); + assertNull(rep.getFirstName()); + assertNull(rep.getLastName()); + assertNull(rep.getAttributes()); + + rawAttributes.put(UserModel.FIRST_NAME, "firstName"); + rawAttributes.put(UserModel.LAST_NAME, "lastName"); + config.setUnmanagedAttributePolicy(UnmanagedAttributePolicy.ADMIN_EDIT); + provider.setConfiguration(JsonSerialization.writeValueAsString(config)); + profile = provider.create(UserProfileContext.USER_API, user); + attributes = profile.getAttributes(); + assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME)); + assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME)); + rep = profile.toRepresentation(); + assertNull(rep.getFirstName()); + assertNull(rep.getLastName()); + assertNull(rep.getAttributes()); + + rawAttributes.remove(UserModel.LAST_NAME); + rawAttributes.put(UserModel.FIRST_NAME, "firstName"); + profile = provider.create(UserProfileContext.USER_API, rawAttributes, user); + attributes = profile.getAttributes(); + assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME)); + assertNull(attributes.getFirst(UserModel.LAST_NAME)); + rep = profile.toRepresentation(); + assertNull(rep.getFirstName()); + assertNull(rep.getLastName()); + assertNull(rep.getAttributes()); + + rawAttributes.put(UserModel.LAST_NAME, "lastNameChanged"); + rawAttributes.put(UserModel.FIRST_NAME, "firstNameChanged"); + profile = provider.create(UserProfileContext.USER_API, rawAttributes, user); + attributes = profile.getAttributes(); + assertEquals(rawAttributes.get(UserModel.FIRST_NAME), attributes.getFirst(UserModel.FIRST_NAME)); + assertEquals(rawAttributes.get(UserModel.LAST_NAME), attributes.getFirst(UserModel.LAST_NAME)); + rep = profile.toRepresentation(); + assertNull(rep.getFirstName()); + assertNull(rep.getLastName()); + assertNull(rep.getAttributes()); + } + @Test public void testAttributeNormalization() { getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeNormalization); @@ -1705,7 +1790,7 @@ private static void testAttributeNormalization(KeycloakSession session) { attributes.put(UserModel.EMAIL, "TesT@TesT.org"); UserProfile profile = provider.create(UserProfileContext.USER_API, attributes); Attributes profileAttributes = profile.getAttributes(); - assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirstValue(UserModel.USERNAME)); - assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirstValue(UserModel.EMAIL)); + assertEquals(attributes.get(UserModel.USERNAME).toLowerCase(), profileAttributes.getFirst(UserModel.USERNAME)); + assertEquals(attributes.get(UserModel.EMAIL).toLowerCase(), profileAttributes.getFirst(UserModel.EMAIL)); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java index 05183e1d3e4b..5e43eb90e89a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/x509/X509DirectGrantTest.java @@ -267,8 +267,6 @@ public void loginCertificateNotExpired() throws Exception { @Test public void loginCertificateExpired() throws Exception { - Assume.assumeFalse("Time offset is causing integer overflow. With the old store it works, because root authentication session has also timestamp overflown, this is not true for the new store so the test is failing.", keycloakUsingProviderWithId(AuthenticationSessionProvider.class, "map")); - X509AuthenticatorConfigModel config = new X509AuthenticatorConfigModel() .setCertValidationEnabled(true) diff --git a/themes/src/main/resources/theme/keycloak.v2/account/src/.babelrc b/themes/src/main/resources/theme/keycloak.v2/account/src/.babelrc index 875902003d8d..6d1276e1e085 100644 --- a/themes/src/main/resources/theme/keycloak.v2/account/src/.babelrc +++ b/themes/src/main/resources/theme/keycloak.v2/account/src/.babelrc @@ -3,7 +3,7 @@ [ "snowpack/assets/babel-plugin.js", { - "webModulesUrl": "./resources/web_modules", + "webModulesUrl": "./keycloak.v2/web_modules", "moduleResolution": "node" } ], diff --git a/themes/src/main/resources/theme/keycloak/common/resources/package.json b/themes/src/main/resources/theme/keycloak/common/resources/package.json index be82339e2869..6760d2677285 100644 --- a/themes/src/main/resources/theme/keycloak/common/resources/package.json +++ b/themes/src/main/resources/theme/keycloak/common/resources/package.json @@ -20,7 +20,7 @@ "@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", - "rollup": "^4.5.1", + "rollup": "^4.8.0", "shx": "^0.3.4" } } diff --git a/themes/src/main/resources/theme/keycloak/common/resources/pnpm-lock.yaml b/themes/src/main/resources/theme/keycloak/common/resources/pnpm-lock.yaml index b8409ae26a44..ff91b52c757c 100644 --- a/themes/src/main/resources/theme/keycloak/common/resources/pnpm-lock.yaml +++ b/themes/src/main/resources/theme/keycloak/common/resources/pnpm-lock.yaml @@ -30,19 +30,19 @@ dependencies: devDependencies: '@rollup/plugin-commonjs': specifier: ^25.0.7 - version: 25.0.7(rollup@4.5.1) + version: 25.0.7(rollup@4.8.0) '@rollup/plugin-node-resolve': specifier: ^15.2.3 - version: 15.2.3(rollup@4.5.1) + version: 15.2.3(rollup@4.8.0) '@rollup/plugin-replace': specifier: ^5.0.5 - version: 5.0.5(rollup@4.5.1) + version: 5.0.5(rollup@4.8.0) '@rollup/plugin-terser': specifier: ^0.4.4 - version: 0.4.4(rollup@4.5.1) + version: 0.4.4(rollup@4.8.0) rollup: - specifier: ^4.5.1 - version: 4.5.1 + specifier: ^4.8.0 + version: 4.8.0 shx: specifier: ^0.3.4 version: 0.3.4 @@ -129,7 +129,7 @@ packages: resolution: {integrity: sha512-h+ducOLDMSxcuec3+YY3x+stM5ZUSnrl/lC/eVmjypil2El08NuE2MNEPMQWdhrod6VRRZFMNqZw/m82iv6U1A==} dev: false - /@rollup/plugin-commonjs@25.0.7(rollup@4.5.1): + /@rollup/plugin-commonjs@25.0.7(rollup@4.8.0): resolution: {integrity: sha512-nEvcR+LRjEjsaSsc4x3XZfCCvZIaSMenZu/OiwOKGN2UhQpAYI7ru7czFvyWbErlpoGjnSX3D5Ch5FcMA3kRWQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -138,16 +138,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.5.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) commondir: 1.0.1 estree-walker: 2.0.2 glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.5 - rollup: 4.5.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-node-resolve@15.2.3(rollup@4.5.1): + /@rollup/plugin-node-resolve@15.2.3(rollup@4.8.0): resolution: {integrity: sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -156,16 +156,16 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.5.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-builtin-module: 3.2.1 is-module: 1.0.0 resolve: 1.22.8 - rollup: 4.5.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-replace@5.0.5(rollup@4.5.1): + /@rollup/plugin-replace@5.0.5(rollup@4.8.0): resolution: {integrity: sha512-rYO4fOi8lMaTg/z5Jb+hKnrHHVn8j2lwkqwyS4kTRhKyWOLf2wST2sWXr4WzWiTcoHTp2sTjqUbqIj2E39slKQ==} engines: {node: '>=14.0.0'} peerDependencies: @@ -174,12 +174,12 @@ packages: rollup: optional: true dependencies: - '@rollup/pluginutils': 5.0.5(rollup@4.5.1) + '@rollup/pluginutils': 5.0.5(rollup@4.8.0) magic-string: 0.30.5 - rollup: 4.5.1 + rollup: 4.8.0 dev: true - /@rollup/plugin-terser@0.4.4(rollup@4.5.1): + /@rollup/plugin-terser@0.4.4(rollup@4.8.0): resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} engines: {node: '>=14.0.0'} peerDependencies: @@ -188,13 +188,13 @@ packages: rollup: optional: true dependencies: - rollup: 4.5.1 + rollup: 4.8.0 serialize-javascript: 6.0.1 smob: 1.4.1 terser: 5.24.0 dev: true - /@rollup/pluginutils@5.0.5(rollup@4.5.1): + /@rollup/pluginutils@5.0.5(rollup@4.8.0): resolution: {integrity: sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==} engines: {node: '>=14.0.0'} peerDependencies: @@ -206,99 +206,107 @@ packages: '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 - rollup: 4.5.1 + rollup: 4.8.0 dev: true - /@rollup/rollup-android-arm-eabi@4.5.1: - resolution: {integrity: sha512-YaN43wTyEBaMqLDYeze+gQ4ZrW5RbTEGtT5o1GVDkhpdNcsLTnLRcLccvwy3E9wiDKWg9RIhuoy3JQKDRBfaZA==} + /@rollup/rollup-android-arm-eabi@4.8.0: + resolution: {integrity: sha512-zdTObFRoNENrdPpnTNnhOljYIcOX7aI7+7wyrSpPFFIOf/nRdedE6IYsjaBE7tjukphh1tMTojgJ7p3lKY8x6Q==} cpu: [arm] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-android-arm64@4.5.1: - resolution: {integrity: sha512-n1bX+LCGlQVuPlCofO0zOKe1b2XkFozAVRoczT+yxWZPGnkEAKTTYVOGZz8N4sKuBnKMxDbfhUsB1uwYdup/sw==} + /@rollup/rollup-android-arm64@4.8.0: + resolution: {integrity: sha512-aiItwP48BiGpMFS9Znjo/xCNQVwTQVcRKkFKsO81m8exrGjHkCBDvm9PHay2kpa8RPnZzzKcD1iQ9KaLY4fPQQ==} cpu: [arm64] os: [android] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-arm64@4.5.1: - resolution: {integrity: sha512-QqJBumdvfBqBBmyGHlKxje+iowZwrHna7pokj/Go3dV1PJekSKfmjKrjKQ/e6ESTGhkfPNLq3VXdYLAc+UtAQw==} + /@rollup/rollup-darwin-arm64@4.8.0: + resolution: {integrity: sha512-zhNIS+L4ZYkYQUjIQUR6Zl0RXhbbA0huvNIWjmPc2SL0cB1h5Djkcy+RZ3/Bwszfb6vgwUvcVJYD6e6Zkpsi8g==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-darwin-x64@4.5.1: - resolution: {integrity: sha512-RrkDNkR/P5AEQSPkxQPmd2ri8WTjSl0RYmuFOiEABkEY/FSg0a4riihWQGKDJ4LnV9gigWZlTMx2DtFGzUrYQw==} + /@rollup/rollup-darwin-x64@4.8.0: + resolution: {integrity: sha512-A/FAHFRNQYrELrb/JHncRWzTTXB2ticiRFztP4ggIUAfa9Up1qfW8aG2w/mN9jNiZ+HB0t0u0jpJgFXG6BfRTA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.5.1: - resolution: {integrity: sha512-ZFPxvUZmE+fkB/8D9y/SWl/XaDzNSaxd1TJUSE27XAKlRpQ2VNce/86bGd9mEUgL3qrvjJ9XTGwoX0BrJkYK/A==} + /@rollup/rollup-linux-arm-gnueabihf@4.8.0: + resolution: {integrity: sha512-JsidBnh3p2IJJA4/2xOF2puAYqbaczB3elZDT0qHxn362EIoIkq7hrR43Xa8RisgI6/WPfvb2umbGsuvf7E37A==} cpu: [arm] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-gnu@4.5.1: - resolution: {integrity: sha512-FEuAjzVIld5WVhu+M2OewLmjmbXWd3q7Zcx+Rwy4QObQCqfblriDMMS7p7+pwgjZoo9BLkP3wa9uglQXzsB9ww==} + /@rollup/rollup-linux-arm64-gnu@4.8.0: + resolution: {integrity: sha512-hBNCnqw3EVCkaPB0Oqd24bv8SklETptQWcJz06kb9OtiShn9jK1VuTgi7o4zPSt6rNGWQOTDEAccbk0OqJmS+g==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-arm64-musl@4.5.1: - resolution: {integrity: sha512-f5Gs8WQixqGRtI0Iq/cMqvFYmgFzMinuJO24KRfnv7Ohi/HQclwrBCYkzQu1XfLEEt3DZyvveq9HWo4bLJf1Lw==} + /@rollup/rollup-linux-arm64-musl@4.8.0: + resolution: {integrity: sha512-Fw9ChYfJPdltvi9ALJ9wzdCdxGw4wtq4t1qY028b2O7GwB5qLNSGtqMsAel1lfWTZvf4b6/+4HKp0GlSYg0ahA==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-gnu@4.5.1: - resolution: {integrity: sha512-CWPkPGrFfN2vj3mw+S7A/4ZaU3rTV7AkXUr08W9lNP+UzOvKLVf34tWCqrKrfwQ0NTk5GFqUr2XGpeR2p6R4gw==} + /@rollup/rollup-linux-riscv64-gnu@4.8.0: + resolution: {integrity: sha512-BH5xIh7tOzS9yBi8dFrCTG8Z6iNIGWGltd3IpTSKp6+pNWWO6qy8eKoRxOtwFbMrid5NZaidLYN6rHh9aB8bEw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.8.0: + resolution: {integrity: sha512-PmvAj8k6EuWiyLbkNpd6BLv5XeYFpqWuRvRNRl80xVfpGXK/z6KYXmAgbI4ogz7uFiJxCnYcqyvZVD0dgFog7Q==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-linux-x64-musl@4.5.1: - resolution: {integrity: sha512-ZRETMFA0uVukUC9u31Ed1nx++29073goCxZtmZARwk5aF/ltuENaeTtRVsSQzFlzdd4J6L3qUm+EW8cbGt0CKQ==} + /@rollup/rollup-linux-x64-musl@4.8.0: + resolution: {integrity: sha512-mdxnlW2QUzXwY+95TuxZ+CurrhgrPAMveDWI97EQlA9bfhR8tw3Pt7SUlc/eSlCNxlWktpmT//EAA8UfCHOyXg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-arm64-msvc@4.5.1: - resolution: {integrity: sha512-ihqfNJNb2XtoZMSCPeoo0cYMgU04ksyFIoOw5S0JUVbOhafLot+KD82vpKXOurE2+9o/awrqIxku9MRR9hozHQ==} + /@rollup/rollup-win32-arm64-msvc@4.8.0: + resolution: {integrity: sha512-ge7saUz38aesM4MA7Cad8CHo0Fyd1+qTaqoIo+Jtk+ipBi4ATSrHWov9/S4u5pbEQmLjgUjB7BJt+MiKG2kzmA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-ia32-msvc@4.5.1: - resolution: {integrity: sha512-zK9MRpC8946lQ9ypFn4gLpdwr5a01aQ/odiIJeL9EbgZDMgbZjjT/XzTqJvDfTmnE1kHdbG20sAeNlpc91/wbg==} + /@rollup/rollup-win32-ia32-msvc@4.8.0: + resolution: {integrity: sha512-p9E3PZlzurhlsN5h9g7zIP1DnqKXJe8ZUkFwAazqSvHuWfihlIISPxG9hCHCoA+dOOspL/c7ty1eeEVFTE0UTw==} cpu: [ia32] os: [win32] requiresBuild: true dev: true optional: true - /@rollup/rollup-win32-x64-msvc@4.5.1: - resolution: {integrity: sha512-5I3Nz4Sb9TYOtkRwlH0ow+BhMH2vnh38tZ4J4mggE48M/YyJyp/0sPSxhw1UeS1+oBgQ8q7maFtSeKpeRJu41Q==} + /@rollup/rollup-win32-x64-msvc@4.8.0: + resolution: {integrity: sha512-kb4/auKXkYKqlUYTE8s40FcJIj5soOyRLHKd4ugR0dCq0G2EfcF54eYcfQiGkHzjidZ40daB4ulsFdtqNKZtBg==} cpu: [x64] os: [win32] requiresBuild: true @@ -1093,23 +1101,24 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true - /rollup@4.5.1: - resolution: {integrity: sha512-0EQribZoPKpb5z1NW/QYm3XSR//Xr8BeEXU49Lc/mQmpmVVG5jPUVrpc2iptup/0WMrY9mzas0fxH+TjYvG2CA==} + /rollup@4.8.0: + resolution: {integrity: sha512-NpsklK2fach5CdI+PScmlE5R4Ao/FSWtF7LkoIrHDxPACY/xshNasPsbpG0VVHxUTbf74tJbVT4PrP8JsJ6ZDA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.5.1 - '@rollup/rollup-android-arm64': 4.5.1 - '@rollup/rollup-darwin-arm64': 4.5.1 - '@rollup/rollup-darwin-x64': 4.5.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.5.1 - '@rollup/rollup-linux-arm64-gnu': 4.5.1 - '@rollup/rollup-linux-arm64-musl': 4.5.1 - '@rollup/rollup-linux-x64-gnu': 4.5.1 - '@rollup/rollup-linux-x64-musl': 4.5.1 - '@rollup/rollup-win32-arm64-msvc': 4.5.1 - '@rollup/rollup-win32-ia32-msvc': 4.5.1 - '@rollup/rollup-win32-x64-msvc': 4.5.1 + '@rollup/rollup-android-arm-eabi': 4.8.0 + '@rollup/rollup-android-arm64': 4.8.0 + '@rollup/rollup-darwin-arm64': 4.8.0 + '@rollup/rollup-darwin-x64': 4.8.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.8.0 + '@rollup/rollup-linux-arm64-gnu': 4.8.0 + '@rollup/rollup-linux-arm64-musl': 4.8.0 + '@rollup/rollup-linux-riscv64-gnu': 4.8.0 + '@rollup/rollup-linux-x64-gnu': 4.8.0 + '@rollup/rollup-linux-x64-musl': 4.8.0 + '@rollup/rollup-win32-arm64-msvc': 4.8.0 + '@rollup/rollup-win32-ia32-msvc': 4.8.0 + '@rollup/rollup-win32-x64-msvc': 4.8.0 fsevents: 2.3.3 dev: true