From 2be5f528e45d89778f8bbdea0556806a1d20d569 Mon Sep 17 00:00:00 2001 From: Vlastimil Elias Date: Mon, 23 Aug 2021 14:23:39 +0200 Subject: [PATCH] KEYCLOAK-18700 - consistently record User profile attribute changes in UPDATE_PROFILE event --- .../java/org/keycloak/events/Details.java | 16 ++-- .../org/keycloak/events/EventBuilder.java | 31 +++++++ .../userprofile/AttributeChangeListener.java | 43 +++++++++ .../userprofile/DefaultUserProfile.java | 15 ++-- .../org/keycloak/userprofile/UserProfile.java | 4 +- .../broker/IdpReviewProfileAuthenticator.java | 7 +- .../requiredactions/UpdateProfile.java | 20 +---- .../resources/account/AccountFormService.java | 19 +--- .../resources/account/AccountRestService.java | 6 +- .../EventAuditingAttributeChangeListener.java | 63 +++++++++++++ .../account/AccountFormServiceTest.java | 6 +- .../account/AccountRestServiceTest.java | 62 ++++++++++++- ...AccountRestServiceWithUserProfileTest.java | 80 +++++++++++++++++ .../RequiredActionUpdateProfileTest.java | 3 +- ...ctionUpdateProfileWithUserProfileTest.java | 13 +++ .../broker/AbstractFirstBrokerLoginTest.java | 89 ++++++++++++++++++- .../testsuite/forms/VerifyProfileTest.java | 7 +- .../user/profile/UserProfileTest.java | 38 ++++---- 18 files changed, 451 insertions(+), 71 deletions(-) create mode 100644 server-spi-private/src/main/java/org/keycloak/userprofile/AttributeChangeListener.java create mode 100644 services/src/main/java/org/keycloak/userprofile/EventAuditingAttributeChangeListener.java diff --git a/server-spi-private/src/main/java/org/keycloak/events/Details.java b/server-spi-private/src/main/java/org/keycloak/events/Details.java index dac012cd95..bf337e709b 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/Details.java +++ b/server-spi-private/src/main/java/org/keycloak/events/Details.java @@ -21,10 +21,14 @@ package org.keycloak.events; * @author Stian Thorgersen */ public interface Details { + String PREF_PREVIOUS = "previous_"; + String PREF_UPDATED = "updated_"; + String CUSTOM_REQUIRED_ACTION="custom_required_action"; + String CONTEXT = "context"; String EMAIL = "email"; - String PREVIOUS_EMAIL = "previous_email"; - String UPDATED_EMAIL = "updated_email"; + String PREVIOUS_EMAIL = PREF_PREVIOUS + "email"; + String UPDATED_EMAIL = PREF_UPDATED + "email"; String ACTION = "action"; String CODE_ID = "code_id"; String REDIRECT_URI = "redirect_uri"; @@ -39,10 +43,10 @@ public interface Details { String USERNAME = "username"; String FIRST_NAME = "first_name"; String LAST_NAME = "last_name"; - String PREVIOUS_FIRST_NAME = "previous_first_name"; - String UPDATED_FIRST_NAME = "updated_first_name"; - String PREVIOUS_LAST_NAME = "previous_last_name"; - String UPDATED_LAST_NAME = "updated_last_name"; + String PREVIOUS_FIRST_NAME = PREF_PREVIOUS + "first_name"; + String UPDATED_FIRST_NAME = PREF_UPDATED + "first_name"; + String PREVIOUS_LAST_NAME = PREF_PREVIOUS + "last_name"; + String UPDATED_LAST_NAME = PREF_UPDATED + "last_name"; String REMEMBER_ME = "remember_me"; String TOKEN_ID = "token_id"; String REFRESH_TOKEN_ID = "refresh_token_id"; diff --git a/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java b/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java index 06560323dd..b9c5c6ff7b 100755 --- a/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java +++ b/server-spi-private/src/main/java/org/keycloak/events/EventBuilder.java @@ -26,12 +26,14 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; /** * @author Stian Thorgersen @@ -145,6 +147,35 @@ public class EventBuilder { event.getDetails().put(key, value); return this; } + + /** + * Add event detail where strings from the input Collection are filtered not to contain null and then joined using :: character. + * + * @param key of the detail + * @param value, can be null + * @return builder for chaining + */ + public EventBuilder detail(String key, Collection values) { + if (values == null || values.isEmpty()) { + return this; + } + return detail(key, values.stream().filter(Objects::nonNull).collect(Collectors.joining("::"))); + } + + /** + * Add event detail where strings from the input Stream are filtered not to contain null and then joined using :: character. + * + * @param key of the detail + * @param value, can be null + * @return builder for chaining + */ + public EventBuilder detail(String key, Stream values) { + if (values == null) { + return this; + } + return detail(key, values.filter(Objects::nonNull).collect(Collectors.joining("::"))); + } + public EventBuilder removeDetail(String key) { if (event.getDetails() != null) { diff --git a/server-spi-private/src/main/java/org/keycloak/userprofile/AttributeChangeListener.java b/server-spi-private/src/main/java/org/keycloak/userprofile/AttributeChangeListener.java new file mode 100644 index 0000000000..8471a2035a --- /dev/null +++ b/server-spi-private/src/main/java/org/keycloak/userprofile/AttributeChangeListener.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 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.userprofile; + +import java.util.List; + +import org.keycloak.models.UserModel; + +/** + * Interface of the user profile attribute change listener. + * + * @author Vlastimil Elias + * + * @see UserProfile#update(boolean, AttributeChangeListener...) + * @see UserProfile#update(AttributeChangeListener...) + */ +@FunctionalInterface +public interface AttributeChangeListener { + + /** + * Method called for each user attribute change. + * + * @param name of the changed user attribute + * @param user model where new attribute value is applied already (can be null if attribute is removed) + * @param oldValue of the attribute before the change (can be null) + */ + void onChange(String name, UserModel user, List oldValue); + +} 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 a0b876687d..bd691e1fbe 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 @@ -24,7 +24,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.function.BiConsumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -89,7 +88,7 @@ public final class DefaultUserProfile implements UserProfile { } @Override - public void update(boolean removeAttributes, BiConsumer... changeListener) { + public void update(boolean removeAttributes, AttributeChangeListener... changeListener) { if (!validated) { validate(); } @@ -97,7 +96,7 @@ public final class DefaultUserProfile implements UserProfile { updateInternal(user, removeAttributes, changeListener); } - private UserModel updateInternal(UserModel user, boolean removeAttributes, BiConsumer... changeListener) { + private UserModel updateInternal(UserModel user, boolean removeAttributes, AttributeChangeListener... changeListener) { if (user == null) { throw new RuntimeException("No user model provided for persisting changes"); } @@ -120,8 +119,8 @@ public final class DefaultUserProfile implements UserProfile { user.setEmailVerified(false); } - for (BiConsumer listener : changeListener) { - listener.accept(name, user); + for (AttributeChangeListener listener : changeListener) { + listener.onChange(name, user, currentValue); } } } @@ -138,7 +137,13 @@ public final class DefaultUserProfile implements UserProfile { if (this.attributes.isReadOnly(attr)) { continue; } + + List currentValue = user.getAttributeStream(attr).filter(Objects::nonNull).collect(Collectors.toList()); user.removeAttribute(attr); + + for (AttributeChangeListener listener : changeListener) { + listener.onChange(attr, user, currentValue); + } } } } catch (ModelException me) { 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 689c153c2c..34c9ee05c5 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 @@ -66,7 +66,7 @@ public interface UserProfile { * @param changeListener a set of one or more listeners to listen for attribute changes * @throws ValidationException in case of any validation error */ - void update(boolean removeAttributes, BiConsumer... changeListener) throws ValidationException; + void update(boolean removeAttributes, AttributeChangeListener... changeListener) throws ValidationException; /** *

The same as {@link #update(boolean, BiConsumer[])} but forcing the removal of attributes. @@ -74,7 +74,7 @@ public interface UserProfile { * @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(BiConsumer... changeListener) throws ValidationException, RuntimeException { + default void update(AttributeChangeListener... changeListener) throws ValidationException, RuntimeException { update(true, changeListener); } 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 2e7f07b828..5eae5f8863 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 @@ -107,7 +107,8 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { @Override protected void actionImpl(AuthenticationFlowContext context, SerializedBrokeredIdentityContext userCtx, BrokeredIdentityContext brokerContext) { EventBuilder event = context.getEvent(); - event.event(EventType.UPDATE_PROFILE); + //velias: looks like UPDATE_PROFILE event is not fired. IMHO it should not be fired here as user record in keycloak is not changed, user doesn't exist yet + event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); UserModelDelegate updatedProfile = new UserModelDelegate(null) { @@ -153,10 +154,10 @@ public class IdpReviewProfileAuthenticator extends AbstractIdpAuthenticator { try { String oldEmail = userCtx.getEmail(); - profile.update((attributeName, userModel) -> { + 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.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().getFirstValue(UserModel.EMAIL)).success(); } }); } catch (ValidationException pve) { diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java index 8a538e4708..a9300065be 100644 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/UpdateProfile.java @@ -37,6 +37,7 @@ import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.ValidationException; import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -64,29 +65,16 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact @Override public void processAction(RequiredActionContext context) { EventBuilder event = context.getEvent(); - event.event(EventType.UPDATE_PROFILE); + event.event(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.UPDATE_PROFILE.name()); MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); UserModel user = context.getUser(); - String oldFirstName = user.getFirstName(); - String oldLastName = user.getLastName(); - String oldEmail = user.getEmail(); UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class); UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, user); try { // backward compatibility with old account console where attributes are not removed if missing - profile.update(false, (attributeName, userModel) -> { - if (attributeName.equals(UserModel.FIRST_NAME)) { - event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, user.getFirstName()); - } - if (attributeName.equals(UserModel.LAST_NAME)) { - event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, user.getLastName()); - } - if (attributeName.equals(UserModel.EMAIL)) { - event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, user.getEmail()); - } - }); + profile.update(false, new EventAuditingAttributeChangeListener(profile, event)); context.success(); } catch (ValidationException pve) { @@ -95,7 +83,7 @@ public class UpdateProfile implements RequiredActionProvider, RequiredActionFact context.challenge(createResponse(context, formData, errors)); } } - + protected UserModel.RequiredAction getResponseAction(){ return UserModel.RequiredAction.UPDATE_PROFILE; } diff --git a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java index b5e9cb2cdc..afa0ca5f76 100755 --- a/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java +++ b/services/src/main/java/org/keycloak/services/resources/account/AccountFormService.java @@ -76,6 +76,7 @@ import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.ValidationException; import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.keycloak.util.JsonSerialization; import org.keycloak.utils.CredentialHelper; @@ -364,28 +365,14 @@ public class AccountFormService extends AbstractSecuredLocalService { UserModel user = auth.getUser(); - String oldFirstName = user.getFirstName(); - String oldLastName = user.getLastName(); - String oldEmail = user.getEmail(); - - event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); + event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).detail(Details.CONTEXT, UserProfileContext.ACCOUNT_OLD.name()); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT_OLD, formData, user); try { // backward compatibility with old account console where attributes are not removed if missing - profile.update(false, (attributeName, userModel) -> { - if (attributeName.equals(UserModel.EMAIL)) { - event.detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, user.getEmail()).success(); - } - if (attributeName.equals(UserModel.FIRST_NAME)) { - event.detail(Details.PREVIOUS_FIRST_NAME, oldFirstName).detail(Details.UPDATED_FIRST_NAME, user.getFirstName()); - } - if (attributeName.equals(UserModel.LAST_NAME)) { - event.detail(Details.PREVIOUS_LAST_NAME, oldLastName).detail(Details.UPDATED_LAST_NAME, user.getLastName()); - } - }); + profile.update(false, new EventAuditingAttributeChangeListener(profile, event)); } catch (ValidationException pve) { List errors = Validation.getFormErrorsFromValidation(pve.getErrors()); 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 418bfdf6f0..c3006d4187 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 @@ -52,6 +52,7 @@ import org.keycloak.common.ClientConnection; import org.keycloak.common.Profile; import org.keycloak.common.enums.AccountRestApiVersion; import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventStoreProvider; import org.keycloak.events.EventType; @@ -85,6 +86,7 @@ import org.keycloak.userprofile.Attributes; import org.keycloak.userprofile.UserProfile; import org.keycloak.userprofile.UserProfileContext; import org.keycloak.userprofile.UserProfileProvider; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.keycloak.userprofile.ValidationException; import org.keycloak.userprofile.ValidationException.Error; import org.keycloak.validate.Validators; @@ -199,14 +201,14 @@ public class AccountRestService { public Response updateAccount(UserRepresentation rep) { auth.require(AccountRoles.MANAGE_ACCOUNT); - event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()); + event.event(EventType.UPDATE_PROFILE).client(auth.getClient()).user(auth.getUser()).detail(Details.CONTEXT, UserProfileContext.ACCOUNT.name()); UserProfileProvider profileProvider = session.getProvider(UserProfileProvider.class); UserProfile profile = profileProvider.create(UserProfileContext.ACCOUNT, rep.toAttributes(), auth.getUser()); try { - profile.update(); + profile.update(new EventAuditingAttributeChangeListener(profile, event)); event.success(); diff --git a/services/src/main/java/org/keycloak/userprofile/EventAuditingAttributeChangeListener.java b/services/src/main/java/org/keycloak/userprofile/EventAuditingAttributeChangeListener.java new file mode 100644 index 0000000000..1803c642af --- /dev/null +++ b/services/src/main/java/org/keycloak/userprofile/EventAuditingAttributeChangeListener.java @@ -0,0 +1,63 @@ +/* + * Copyright 2021 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.userprofile; + +import java.util.List; + +import org.keycloak.events.Details; +import org.keycloak.events.Event; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.UserModel; + +/** + * {@link AttributeChangeListener} to audit user profile attribute changes into {@link Event}. + * + * Adds info about user profile attribute change into {@link Event}'s detail field. + * + * @author Vlastimil Elias + * + * @see UserProfile#update(AttributeChangeListener...) + */ +public class EventAuditingAttributeChangeListener implements AttributeChangeListener { + + private EventBuilder event; + private UserProfile profile; + + /** + * @param profile used to read attribute configuration from + * @param event to add detail info into + */ + public EventAuditingAttributeChangeListener(UserProfile profile, EventBuilder event) { + super(); + this.profile = profile; + this.event = event; + } + + @Override + public void onChange(String attributeName, UserModel userModel, List oldValue) { + if (attributeName.equals(UserModel.FIRST_NAME)) { + event.detail(Details.PREVIOUS_FIRST_NAME, oldValue).detail(Details.UPDATED_FIRST_NAME, userModel.getFirstName()); + } else if (attributeName.equals(UserModel.LAST_NAME)) { + event.detail(Details.PREVIOUS_LAST_NAME, oldValue).detail(Details.UPDATED_LAST_NAME, userModel.getLastName()); + } else if (attributeName.equals(UserModel.EMAIL)) { + event.detail(Details.PREVIOUS_EMAIL, oldValue).detail(Details.UPDATED_EMAIL, userModel.getEmail()); + } else { + event.detail(Details.PREF_PREVIOUS + attributeName, oldValue).detail(Details.PREF_UPDATED + attributeName, userModel.getAttributeStream(attributeName)); + } + } + +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java index 52e3f16732..7af8ee3333 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/AccountFormServiceTest.java @@ -70,6 +70,8 @@ import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UIUtils; import org.keycloak.testsuite.util.URLUtils; import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.userprofile.UserProfileContext; + import java.util.Collections; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; @@ -713,7 +715,9 @@ public class AccountFormServiceTest extends AbstractTestRealmKeycloakTest { Assert.assertEquals("New last", profilePage.getLastName()); Assert.assertEquals("new@email.com", profilePage.getEmail()); - events.expectAccount(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") + events.expectAccount(EventType.UPDATE_PROFILE) + .detail(Details.CONTEXT, UserProfileContext.ACCOUNT_OLD.name()) + .detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "New first") .detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "New last") .detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com") .assertEvent(); 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 e513a83faf..0587d739cb 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 @@ -18,6 +18,7 @@ package org.keycloak.testsuite.account; import com.fasterxml.jackson.core.type.TypeReference; import org.junit.Assert; +import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.admin.client.resource.UserResource; @@ -30,6 +31,8 @@ import org.keycloak.common.Profile; import org.keycloak.common.enums.AccountRestApiVersion; import org.keycloak.common.util.ObjectUtil; import org.keycloak.credential.CredentialTypeMetadata; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.UserModel; import org.keycloak.models.credential.OTPCredentialModel; @@ -55,6 +58,7 @@ import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentati import org.keycloak.services.messages.Messages; import org.keycloak.services.resources.account.AccountCredentialResource; import org.keycloak.services.util.ResolveRelative; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; @@ -63,6 +67,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.TokenUtil; import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.userprofile.UserProfileContext; import org.keycloak.validate.validators.EmailValidator; import javax.ws.rs.core.Response; @@ -88,6 +93,9 @@ import static org.junit.Assert.assertTrue; @EnableFeature(value = Profile.Feature.WEB_AUTHN, skipRestart = true, onlyForProduct = true) public class AccountRestServiceTest extends AbstractRestServiceTest { + @Rule + public AssertEvents events = new AssertEvents(this); + @Test public void testGetUserProfileMetadata_EditUsernameAllowed() throws IOException { @@ -249,6 +257,58 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { } + @Test + public void testUpdateProfileEvent() throws IOException { + UserRepresentation user = getUser(); + String originalUsername = user.getUsername(); + String originalFirstName = user.getFirstName(); + String originalLastName = user.getLastName(); + String originalEmail = user.getEmail(); + Map> originalAttributes = new HashMap<>(user.getAttributes()); + + try { + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + + realmRep.setRegistrationEmailAsUsername(false); + adminClient.realm("test").update(realmRep); + + 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 = 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) + .detail(Details.UPDATED_EMAIL, "bobby@localhost") + .detail(Details.PREVIOUS_FIRST_NAME, originalFirstName) + .detail(Details.PREVIOUS_LAST_NAME, originalLastName) + .detail(Details.UPDATED_FIRST_NAME, "Homer") + .detail(Details.UPDATED_LAST_NAME, "Simpsons") + .assertEvent(); + events.assertEmpty(); + + } finally { + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + realmRep.setEditUsernameAllowed(true); + adminClient.realm("test").update(realmRep); + + user.setUsername(originalUsername); + 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()); + } + } + @Test public void testUpdateProfile() throws IOException { UserRepresentation user = getUser(); @@ -278,7 +338,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest { assertEquals("val1", user.getAttributes().get("attr1").get(0)); assertEquals(1, user.getAttributes().get("attr2").size()); assertEquals("val2", user.getAttributes().get("attr2").get(0)); - + // Update attributes user.getAttributes().remove("attr1"); user.getAttributes().get("attr2").add("val3"); 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 cdc8a65b82..83ed4c17e9 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 @@ -25,17 +25,25 @@ import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_E import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_ONLY; import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; +import org.keycloak.broker.provider.util.SimpleHttp; import org.keycloak.common.Profile; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.representations.account.UserProfileAttributeMetadata; import org.keycloak.representations.account.UserRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.forms.VerifyProfileTest; +import org.keycloak.userprofile.UserProfileContext; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; /** * @@ -164,6 +172,78 @@ public class AccountRestServiceWithUserProfileTest extends AccountRestServiceTes return uam.getValidators().get(validatorId); } + @Test + public void testUpdateProfileEventWithAdditionalAttributesAuditing() throws IOException { + + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"attr1\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"attr2\"," + PERMISSIONS_ALL + "}" + + "]}"); + + UserRepresentation user = getUser(); + String originalUsername = user.getUsername(); + String originalFirstName = user.getFirstName(); + String originalLastName = user.getLastName(); + String originalEmail = user.getEmail(); + Map> originalAttributes = new HashMap<>(user.getAttributes()); + + try { + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + + realmRep.setRegistrationEmailAsUsername(false); + adminClient.realm("test").update(realmRep); + + 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 = 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) + .detail(Details.UPDATED_EMAIL, "bobby@localhost") + .detail(Details.PREVIOUS_FIRST_NAME, originalFirstName) + .detail(Details.PREVIOUS_LAST_NAME, originalLastName) + .detail(Details.UPDATED_FIRST_NAME, "Homer") + .detail(Details.UPDATED_LAST_NAME, "Simpsons") + .detail(Details.PREF_UPDATED+"attr2", "val2") + .assertEvent(); + events.assertEmpty(); + + } finally { + RealmRepresentation realmRep = adminClient.realm("test").toRepresentation(); + realmRep.setEditUsernameAllowed(true); + adminClient.realm("test").update(realmRep); + + user.setUsername(originalUsername); + 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()); + } + } + + @Test + public void testUpdateProfileEvent() throws IOException { + setUserProfileConfiguration("{\"attributes\": [" + + "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"lastName\"," + PERMISSIONS_ALL + ", \"required\": {}}," + + "{\"name\": \"attr1\"," + PERMISSIONS_ALL + "}," + + "{\"name\": \"attr2\"," + PERMISSIONS_ALL + "}" + + "]}"); + super.testUpdateProfileEvent(); + } + @Test @Override public void testUpdateProfile() throws IOException { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java index cc8f7b745d..408632da10 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileTest.java @@ -38,6 +38,7 @@ import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginUpdateProfileEditUsernameAllowedPage; import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.userprofile.UserProfileContext; import static org.junit.Assert.assertFalse; @@ -358,7 +359,7 @@ public class RequiredActionUpdateProfileTest extends AbstractTestRealmKeycloakTe updateProfilePage.update("New first", "New last", "new@email.com", "test-user@localhost"); - events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); + events.expectRequiredAction(EventType.UPDATE_PROFILE).detail(Details.CONTEXT, UserProfileContext.UPDATE_PROFILE.name()).detail(Details.PREVIOUS_EMAIL, "test-user@localhost").detail(Details.UPDATED_EMAIL, "new@email.com").assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java index 85ee47164b..a8d6288b5a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/actions/RequiredActionUpdateProfileWithUserProfileTest.java @@ -47,6 +47,7 @@ import org.keycloak.testsuite.forms.VerifyProfileTest; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.util.ClientScopeBuilder; import org.keycloak.testsuite.util.KeycloakModelUtils; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.openqa.selenium.By; /** @@ -418,6 +419,12 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi //submit OK updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + // we also test additional attribute configured to be audited in the event + events.expectRequiredAction(EventType.UPDATE_PROFILE) + .detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "FirstCC") + .detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "LastCC") + .detail(Details.PREF_UPDATED + "department", "DepartmentCC") + .assertEvent(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); @@ -451,6 +458,12 @@ public class RequiredActionUpdateProfileWithUserProfileTest extends RequiredActi //submit OK updateProfilePage.updateWithDepartment("FirstCC", "LastCC", "DepartmentCC", USERNAME1, USERNAME1); + events.expectRequiredAction(EventType.UPDATE_PROFILE).client(client_scope_optional.getClientId()) + .detail(Details.PREVIOUS_FIRST_NAME, "Tom").detail(Details.UPDATED_FIRST_NAME, "FirstCC") + .detail(Details.PREVIOUS_LAST_NAME, "Brady").detail(Details.UPDATED_LAST_NAME, "LastCC") + .assertEvent(); + + Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE)); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java index 27e18256b7..232e39624b 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/AbstractFirstBrokerLoginTest.java @@ -9,24 +9,30 @@ import com.google.common.collect.ImmutableMap; import org.hamcrest.MatcherAssert; import org.hamcrest.Matchers; import org.jboss.arquillian.drone.api.annotation.Drone; +import org.junit.Rule; import org.junit.Test; import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.models.IdentityProviderMapperModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.EventRepresentation; import org.keycloak.representations.idm.IdentityProviderMapperRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.forms.VerifyProfileTest; import org.keycloak.testsuite.pages.LoginPasswordUpdatePage; import org.keycloak.testsuite.util.MailServer; import org.keycloak.testsuite.util.MailServerConfiguration; import org.keycloak.testsuite.util.SecondBrowser; +import org.keycloak.userprofile.UserProfileContext; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; @@ -56,6 +62,9 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa @SecondBrowser protected WebDriver driver2; + @Rule + public AssertEvents events = new AssertEvents(this); + protected void enableDynamicUserProfile() { RealmResource rr = adminClient.realm(bc.consumerRealmName()); @@ -971,7 +980,85 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa //test if the user has verified email assertTrue(consumerRealm.users().get(linkedUserId).toRepresentation().isEmailVerified()); } + + @Test + public void testEventsOnUpdateProfileNoEmailChange() { + updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin); + createUser(bc.providerRealmName(), "no-first-name", "password", null, "LastName", "no-first-name@localhost.com"); + driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName())); + log.debug("Clicking social " + bc.getIDPAlias()); + loginPage.clickSocial(bc.getIDPAlias()); + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the provider realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + log.debug("Logging in"); + loginPage.login("no-first-name", "password"); + + waitForPage(driver, "update account information", false); + + updateAccountInformationPage.assertCurrent(); + updateAccountInformationPage.updateAccountInformation("FirstName", "LastName"); + waitForAccountManagementTitle(); + accountUpdateProfilePage.assertCurrent(); + Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName()); + Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName()); + Assert.assertEquals("no-first-name@localhost.com", accountUpdateProfilePage.getEmail()); + Assert.assertEquals("no-first-name", accountUpdateProfilePage.getUsername()); + + RealmRepresentation consumerRealmRep = adminClient.realm(bc.consumerRealmName()).toRepresentation(); + events.expectAccount(EventType.LOGIN).realm(consumerRealmRep).user(Matchers.any(String.class)).session(Matchers.any(String.class)) + .detail(Details.IDENTITY_PROVIDER_USERNAME, "no-first-name") + .detail(Details.REGISTER_METHOD, "broker") + .assertEvent(getFirstConsumerEvent()); + } + + @Test + public void testEventsOnUpdateProfileWithEmailChange() { + updateExecutions(AbstractBrokerTest::setUpMissingUpdateProfileOnFirstLogin); + + createUser(bc.providerRealmName(), "no-first-name", "password", null, "LastName", "no-first-name@localhost.com"); + driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName())); + log.debug("Clicking social " + bc.getIDPAlias()); + loginPage.clickSocial(bc.getIDPAlias()); + waitForPage(driver, "sign in to", true); + Assert.assertTrue("Driver should be on the provider realm page right now", + driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/")); + log.debug("Logging in"); + loginPage.login("no-first-name", "password"); + + waitForPage(driver, "update account information", false); + + updateAccountInformationPage.assertCurrent(); + updateAccountInformationPage.updateAccountInformation("new-email@localhost.com","FirstName", "LastName"); + waitForAccountManagementTitle(); + accountUpdateProfilePage.assertCurrent(); + Assert.assertEquals("FirstName", accountUpdateProfilePage.getFirstName()); + Assert.assertEquals("LastName", accountUpdateProfilePage.getLastName()); + Assert.assertEquals("new-email@localhost.com", accountUpdateProfilePage.getEmail()); + Assert.assertEquals("no-first-name", accountUpdateProfilePage.getUsername()); + + RealmRepresentation consumerRealmRep = adminClient.realm(bc.consumerRealmName()).toRepresentation(); + events.expectAccount(EventType.UPDATE_EMAIL).realm(consumerRealmRep).user((String)null).session((String) null) + .detail(Details.CONTEXT, UserProfileContext.IDP_REVIEW.name()) + .detail(Details.IDENTITY_PROVIDER_USERNAME, "no-first-name") + .detail(Details.PREVIOUS_EMAIL, "no-first-name@localhost.com") + .detail(Details.UPDATED_EMAIL, "new-email@localhost.com") + .assertEvent(getFirstConsumerEvent()); + events.expectAccount(EventType.LOGIN).realm(consumerRealmRep).user(Matchers.any(String.class)).session(Matchers.any(String.class)) + .detail(Details.IDENTITY_PROVIDER_USERNAME, "no-first-name") + .detail(Details.REGISTER_METHOD, "broker") + .assertEvent(events.poll()); + } + + protected EventRepresentation getFirstConsumerEvent() { + String providerRealmId = adminClient.realm(bc.providerRealmName()).toRepresentation().getId(); + EventRepresentation er = events.poll(); + while(er != null && providerRealmId.equals(er.getRealmId())) { + er = events.poll(); + } + return er; + } /** * Refers to in old test suite: org.keycloak.testsuite.broker.AbstractKeycloakIdentityProviderTest.testSuccessfulAuthenticationUpdateProfileOnMissing_missingEmail @@ -991,6 +1078,7 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa loginPage.login("no-first-name", "password"); waitForPage(driver, "update account information", false); + updateAccountInformationPage.assertCurrent(); updateAccountInformationPage.updateAccountInformation("FirstName", "LastName"); waitForAccountManagementTitle(); @@ -1000,7 +1088,6 @@ public abstract class AbstractFirstBrokerLoginTest extends AbstractInitializedBa Assert.assertEquals("no-first-name@localhost.com", accountUpdateProfilePage.getEmail()); Assert.assertEquals("no-first-name", accountUpdateProfilePage.getUsername()); - logoutFromRealm(getProviderRoot(), bc.providerRealmName()); logoutFromRealm(getConsumerRoot(), bc.consumerRealmName()); createUser(bc.providerRealmName(), "no-last-name", "password", "FirstName", null, "no-last-name@localhost.com"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java index b91ab8f31d..49f3b686bf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/VerifyProfileTest.java @@ -48,7 +48,6 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.EnableFeature; -import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider; import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage.RequestType; import org.keycloak.testsuite.pages.LoginPage; @@ -58,7 +57,8 @@ import org.keycloak.testsuite.util.KeycloakModelUtils; import org.keycloak.testsuite.util.OAuthClient; import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.UserBuilder; -import org.keycloak.userprofile.UserProfileSpi; +import org.keycloak.userprofile.UserProfileContext; +import org.keycloak.userprofile.EventAuditingAttributeChangeListener; import org.openqa.selenium.By; /** @@ -321,9 +321,12 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest { verifyProfilePage.update("First", "Last", "Department"); //event after profile is updated + // we also test additional attribute configured to be audited in the event events.expectRequiredAction(EventType.UPDATE_PROFILE).user(user5Id) + .detail(Details.CONTEXT, UserProfileContext.UPDATE_PROFILE.name()) .detail(Details.PREVIOUS_FIRST_NAME, "ExistingFirst").detail(Details.UPDATED_FIRST_NAME, "First") .detail(Details.PREVIOUS_LAST_NAME, "ExistingLast").detail(Details.UPDATED_LAST_NAME, "Last") + .detail(Details.PREF_UPDATED+"department", "Department") .assertEvent(); } 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 c8f35dc6ef..cdf7477422 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 @@ -43,7 +43,6 @@ import java.util.function.Consumer; import org.junit.Assert; import org.junit.Test; -import org.keycloak.common.Profile; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; import org.keycloak.models.Constants; @@ -53,13 +52,9 @@ import org.keycloak.models.UserModel; import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.services.messages.Messages; -import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; -import org.keycloak.testsuite.arquillian.annotation.EnableFeature; -import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider; import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.userprofile.AttributeGroupMetadata; import org.keycloak.userprofile.DeclarativeUserProfileProvider; -import org.keycloak.userprofile.UserProfileSpi; import org.keycloak.userprofile.config.UPAttribute; import org.keycloak.userprofile.config.UPAttributePermissions; import org.keycloak.userprofile.config.UPAttributeRequired; @@ -459,8 +454,15 @@ public class UserProfileTest extends AbstractUserProfileTest { profile = provider.create(UserProfileContext.ACCOUNT, attributes, user); Set attributesUpdated = new HashSet<>(); - - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + Map attributesUpdatedOldValues = new HashMap<>(); + attributesUpdatedOldValues.put(UserModel.FIRST_NAME, "Joe"); + attributesUpdatedOldValues.put(UserModel.LAST_NAME, "Doe"); + + profile.update((attributeName, userModel, oldValue) -> { + assertTrue(attributesUpdated.add(attributeName)); + assertEquals(attributesUpdatedOldValues.get(attributeName), getSingleValue(oldValue)); + assertEquals(attributes.get(attributeName), userModel.getFirstAttribute(attributeName)); + }); assertThat(attributesUpdated, containsInAnyOrder(UserModel.FIRST_NAME, UserModel.LAST_NAME, UserModel.EMAIL)); @@ -470,13 +472,19 @@ public class UserProfileTest extends AbstractUserProfileTest { profile = provider.create(UserProfileContext.ACCOUNT, attributes, user); attributesUpdated.clear(); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("business.address")); assertEquals("fixed-business-address", user.getFirstAttribute("business.address")); } - + + private static String getSingleValue(List vals) { + if(vals==null || vals.isEmpty()) + return null; + return vals.get(0); + } + @Test public void testReadonlyUpdates() { getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyUpdates); @@ -505,7 +513,7 @@ public class UserProfileTest extends AbstractUserProfileTest { Set attributesUpdated = new HashSet<>(); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("department")); @@ -556,7 +564,7 @@ public class UserProfileTest extends AbstractUserProfileTest { Set attributesUpdated = new HashSet<>(); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("department", "address", "phone")); provider.setConfiguration("{\"attributes\": [{\"name\": \"department\", \"permissions\": {\"edit\": [\"admin\"]}}," @@ -566,7 +574,7 @@ public class UserProfileTest extends AbstractUserProfileTest { attributes.put("department", "foo"); attributes.put("phone", "foo"); profile = provider.create(UserProfileContext.USER_API, attributes, user); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("department", "phone")); assertTrue(user.getAttributes().containsKey("address")); @@ -578,7 +586,7 @@ public class UserProfileTest extends AbstractUserProfileTest { attributes.put("address", "bar"); attributesUpdated.clear(); profile = provider.create(UserProfileContext.USER_API, attributes, user); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("address")); assertEquals("bar", user.getFirstAttribute("address")); assertEquals("foo", user.getFirstAttribute("phone")); @@ -587,7 +595,7 @@ public class UserProfileTest extends AbstractUserProfileTest { attributes.remove("address"); attributesUpdated.clear(); profile = provider.create(UserProfileContext.USER_API, attributes, user); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertThat(attributesUpdated, containsInAnyOrder("address")); assertFalse(user.getAttributes().containsKey("address")); assertTrue(user.getAttributes().containsKey("phone")); @@ -597,7 +605,7 @@ public class UserProfileTest extends AbstractUserProfileTest { attributes.put(prefixedAttributeName, "foo"); attributesUpdated.clear(); profile = provider.create(UserProfileContext.USER_API, attributes, user); - profile.update((attributeName, userModel) -> assertTrue(attributesUpdated.add(attributeName))); + profile.update((attributeName, userModel, oldValue) -> assertTrue(attributesUpdated.add(attributeName))); assertTrue(attributesUpdated.isEmpty()); assertFalse(user.getAttributes().containsKey("prefixedAttributeName")); }