Updating the UP configuration needs to trigger an admin event

Close #23896

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2023-12-14 13:43:48 +01:00 committed by Marek Posolda
parent bee7595275
commit d841971ff4
14 changed files with 327 additions and 17 deletions

View file

@ -21,6 +21,7 @@ package org.keycloak.representations.userprofile.config;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
/**
* Configuration of the Attribute.
@ -170,4 +171,28 @@ public class UPAttribute implements Cloneable {
attr.setGroup(this.group);
return attr;
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPAttribute other = (UPAttribute) obj;
return Objects.equals(this.name, other.name)
&& Objects.equals(this.displayName, other.displayName)
&& Objects.equals(this.group, other.group)
&& Objects.equals(this.validations, other.validations)
&& Objects.equals(this.annotations, other.annotations)
&& Objects.equals(this.required, other.required)
&& Objects.equals(this.permissions, other.permissions)
&& Objects.equals(this.selector, other.selector);
}
}

View file

@ -20,6 +20,7 @@ package org.keycloak.representations.userprofile.config;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -75,4 +76,22 @@ public class UPAttributePermissions implements Cloneable {
Set<String> edit = this.edit == null ? null : new HashSet<>(this.edit);
return new UPAttributePermissions(view, edit);
}
@Override
public int hashCode() {
return Objects.hash(view, edit);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPAttributePermissions other = (UPAttributePermissions) obj;
return Objects.equals(this.view, other.view)
&& Objects.equals(this.edit, other.edit);
}
}

View file

@ -19,6 +19,7 @@
package org.keycloak.representations.userprofile.config;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -82,4 +83,21 @@ public class UPAttributeRequired implements Cloneable {
return new UPAttributeRequired(roles, scopes);
}
@Override
public int hashCode() {
return Objects.hash(roles, scopes);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPAttributeRequired other = (UPAttributeRequired) obj;
return Objects.equals(this.roles, other.roles)
&& Objects.equals(this.scopes, other.scopes);
}
}

View file

@ -19,6 +19,7 @@
package org.keycloak.representations.userprofile.config;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
@ -56,4 +57,21 @@ public class UPAttributeSelector implements Cloneable {
protected UPAttributeSelector clone() {
return new UPAttributeSelector(scopes == null ? null : new HashSet<>(scopes));
}
@Override
public int hashCode() {
return Objects.hash(scopes);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPAttributeSelector other = (UPAttributeSelector) obj;
return Objects.equals(this.scopes, other.scopes);
}
}

View file

@ -21,6 +21,7 @@ package org.keycloak.representations.userprofile.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -137,4 +138,23 @@ public class UPConfig implements Cloneable {
return cfg;
}
@Override
public int hashCode() {
return Objects.hash(attributes, groups, unmanagedAttributePolicy);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPConfig other = (UPConfig) obj;
return Objects.equals(this.attributes, other.attributes)
&& Objects.equals(this.groups, other.groups)
&& this.unmanagedAttributePolicy == other.unmanagedAttributePolicy;
}
}

View file

@ -21,6 +21,7 @@ package org.keycloak.representations.userprofile.config;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Configuration of the attribute group.
@ -82,4 +83,24 @@ public class UPGroup implements Cloneable {
group.setAnnotations(this.annotations == null ? null : new HashMap<>(this.annotations));
return group;
}
@Override
public int hashCode() {
return Objects.hash(name);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final UPGroup other = (UPGroup) obj;
return Objects.equals(this.name, other.name)
&& Objects.equals(this.displayHeader, other.displayHeader)
&& Objects.equals(this.displayDescription, other.displayDescription)
&& Objects.equals(this.annotations, other.annotations);
}
}

View file

@ -19,8 +19,6 @@
package org.keycloak.admin.ui.rest;
import java.io.IOException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.PUT;
@ -35,7 +33,6 @@ import org.keycloak.services.resources.admin.AdminEventBuilder;
import org.keycloak.services.resources.admin.RealmAdminResource;
import org.keycloak.services.resources.admin.UserProfileResource;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.util.JsonSerialization;
/**
* This JAX-RS resource is decorating the Admin Realm API in order to support specific behaviors from the
@ -48,10 +45,12 @@ public class UIRealmResource {
private final RealmAdminResource delegate;
private final KeycloakSession session;
private final AdminPermissionEvaluator auth;
private final AdminEventBuilder adminEvent;
public UIRealmResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
this.delegate = new RealmAdminResource(session, auth, adminEvent);
}
@ -75,7 +74,9 @@ public class UIRealmResource {
return;
}
Response response = new UserProfileResource(session, auth).update(upConfig);
UserProfileResource userProfileResource = new UserProfileResource(session, auth, adminEvent);
if (!upConfig.equals(userProfileResource.getConfiguration())) {
Response response = userProfileResource.update(upConfig);
if (isSuccessful(response)) {
return;
@ -83,6 +84,7 @@ public class UIRealmResource {
throw new InternalServerErrorException("Failed to update user profile configuration");
}
}
private boolean isSuccessful(Response response) {
return Family.SUCCESSFUL.equals(response.getStatusInfo().getFamily());

View file

@ -186,5 +186,10 @@ public enum ResourceType {
/**
*
*/
, CUSTOM;
, CUSTOM
/**
* The user profile configuration
*/
, USER_PROFILE;
}

View file

@ -36,6 +36,8 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.UserProfileMetadata;
@ -54,14 +56,15 @@ import org.keycloak.representations.userprofile.config.UPConfig;
public class UserProfileResource {
protected final KeycloakSession session;
protected final AdminEventBuilder adminEvent;
protected final RealmModel realm;
private final AdminPermissionEvaluator auth;
public UserProfileResource(KeycloakSession session, AdminPermissionEvaluator auth) {
public UserProfileResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.realm = session.getContext().getRealm();
this.auth = auth;
this.adminEvent = adminEvent.resource(ResourceType.USER_PROFILE);
}
@GET
@ -101,6 +104,11 @@ public class UserProfileResource {
throw ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
}
adminEvent.operation(OperationType.UPDATE)
.resourcePath(session.getContext().getUri())
.representation(config)
.success();
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
}
}

View file

@ -70,7 +70,6 @@ import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.models.Constants.SESSION_NOTE_LIGHTWEIGHT_USER;
import static org.keycloak.models.utils.KeycloakModelUtils.findGroupByPath;
import static org.keycloak.userprofile.UserProfileContext.USER_API;
@ -460,7 +459,7 @@ public class UsersResource {
*/
@Path("profile")
public UserProfileResource userProfile() {
return new UserProfileResource(session, auth);
return new UserProfileResource(session, auth, adminEvent);
}
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {

View file

@ -51,13 +51,13 @@ public class UserTestWithUserProfile extends UserTest {
public void onBefore() throws IOException {
RealmRepresentation realmRep = realm.toRepresentation();
VerifyProfileTest.disableDynamicUserProfile(realm);
assertAdminEvents.poll();
realm.update(realmRep);
assertAdminEvents.poll();
assertAdminEvents.poll(); // update realm
assertAdminEvents.poll(); // set UP configuration
VerifyProfileTest.enableDynamicUserProfile(realmRep);
realm.update(realmRep);
assertAdminEvents.poll();
VerifyProfileTest.setUserProfileConfiguration(realm, null);
assertAdminEvents.poll();
UPConfig upConfig = realm.users().userProfile().getConfiguration();
for (String name : managedAttributes) {
@ -65,6 +65,7 @@ public class UserTestWithUserProfile extends UserTest {
}
VerifyProfileTest.setUserProfileConfiguration(realm, JsonSerialization.writeValueAsString(upConfig));
assertAdminEvents.poll();
}
@Test

View file

@ -44,7 +44,10 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
@ -61,6 +64,8 @@ import org.keycloak.testsuite.pages.AppPage.RequestType;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.VerifyProfilePage;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.util.AdminEventPaths;
import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.testsuite.util.ClientScopeBuilder;
import org.keycloak.testsuite.util.JsonTestUtils;
import org.keycloak.testsuite.util.KeycloakModelUtils;
@ -162,6 +167,9 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this);
@Page
protected AppPage appPage;
@ -1193,7 +1201,14 @@ public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
}
protected UPConfig setUserProfileConfiguration(String configuration) {
return setUserProfileConfiguration(testRealm(), configuration);
assertAdminEvents.clear();
UPConfig result = setUserProfileConfiguration(testRealm(), configuration);
AdminEventRepresentation adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME,
OperationType.UPDATE, AdminEventPaths.userProfilePath(), ResourceType.USER_PROFILE);
Assert.assertTrue("Incorrect representation in event", StringUtils.isBlank(configuration)
? StringUtils.isBlank(adminEvent.getRepresentation())
: StringUtils.isNotBlank(adminEvent.getRepresentation()));
return result;
}
public static void enableDynamicUserProfile(RealmRepresentation testRealm) {

View file

@ -0,0 +1,152 @@
/*
*
* * 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.testsuite.user.profile;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.BearerAuthFilter;
import org.keycloak.admin.ui.rest.model.UIRealmRepresentation;
import org.keycloak.common.Profile;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributePermissions;
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.util.AssertAdminEvents;
import org.keycloak.userprofile.DeclarativeUserProfileProvider;
import org.keycloak.userprofile.config.UPConfigUtils;
import org.keycloak.util.JsonSerialization;
/**
*
* @author rmartinc
*/
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE)
public class UIRealmResourceTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertAdminEvents assertAdminEvents = new AssertAdminEvents(this);
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
if (testRealm.getAttributes() == null) {
testRealm.setAttributes(new HashMap<>());
}
testRealm.getAttributes().put(DeclarativeUserProfileProvider.REALM_USER_PROFILE_ENABLED, Boolean.TRUE.toString());
}
@Test
public void testNoUpdateUserProfile() throws IOException {
RealmRepresentation rep = testRealm().toRepresentation();
updateRealmExt(toUIRealmRepresentation(rep, null));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
assertAdminEvents.assertEmpty();
}
@Test
public void testSameUpdateUserProfile() throws IOException {
RealmRepresentation rep = testRealm().toRepresentation();
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
assertAdminEvents.assertEmpty();
}
@Test
public void testUpdateUserProfileModification() throws IOException {
RealmRepresentation rep = testRealm().toRepresentation();
UPConfig upConfig = testRealm().users().userProfile().getConfiguration();
upConfig.addOrReplaceAttribute(new UPAttribute("foo",
new UPAttributePermissions(Set.of(), Set.of(UPConfigUtils.ROLE_USER, UPConfigUtils.ROLE_ADMIN))));
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
AdminEventRepresentation adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
Assert.assertNotNull(adminEvent.getRepresentation());
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
upConfig.getAttribute("foo").setDisplayName("Foo");
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
upConfig.getAttribute("foo").setPermissions(new UPAttributePermissions(Set.of(), Set.of(UPConfigUtils.ROLE_USER)));
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
upConfig.getAttribute("foo").setRequired(new UPAttributeRequired(Set.of(UPConfigUtils.ROLE_ADMIN, UPConfigUtils.ROLE_USER), Set.of()));
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
upConfig.getAttribute("foo").setValidations(Map.of("length", Map.of("min", "3", "max", "128")));
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
adminEvent = assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, "ui-ext", ResourceType.USER_PROFILE);
Assert.assertEquals(upConfig, toUpConfig(adminEvent.getRepresentation()));
updateRealmExt(toUIRealmRepresentation(rep, upConfig));
assertAdminEvents.assertEvent(TEST_REALM_NAME, OperationType.UPDATE, Matchers.nullValue(String.class), ResourceType.REALM);
assertAdminEvents.assertEmpty();
}
private void updateRealmExt(UIRealmRepresentation rep) {
try (Client client = Keycloak.getClientProvider().newRestEasyClient(null, null, true)) {
Response response = client.target(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth")
.path("/admin/realms/" + rep.getRealm() + "/ui-ext")
.register(new BearerAuthFilter(adminClient.tokenManager()))
.request(MediaType.APPLICATION_JSON)
.put(Entity.entity(rep, MediaType.APPLICATION_JSON));
Assert.assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
}
private UIRealmRepresentation toUIRealmRepresentation(RealmRepresentation realm, UPConfig upConfig) throws IOException {
UIRealmRepresentation uiRealm = JsonSerialization.readValue(JsonSerialization.writeValueAsString(realm), UIRealmRepresentation.class);
uiRealm.setUpConfig(upConfig);
return uiRealm;
}
private UPConfig toUpConfig(String representation) throws IOException {
return JsonSerialization.readValue(representation, UPConfig.class);
}
}

View file

@ -36,6 +36,7 @@ import org.keycloak.admin.client.resource.RoleByIdResource;
import org.keycloak.admin.client.resource.RoleMappingResource;
import org.keycloak.admin.client.resource.RoleResource;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.admin.client.resource.UserProfileResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
@ -69,6 +70,12 @@ public class AdminEventPaths {
return uri.toString();
}
public static String userProfilePath() {
URI uri = UriBuilder.fromUri("").path(RealmResource.class, "users")
.path(UsersResource.class, "userProfile")
.build();
return uri.toString();
}
// CLIENT RESOURCE