Do not require non-builtin attributes for service accounts

Closes #26716

Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
rmartinc 2024-02-12 17:47:48 +01:00 committed by Marek Posolda
parent 6bbf8358b4
commit bb12f3fb82
2 changed files with 49 additions and 15 deletions

View file

@ -105,7 +105,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes, protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes,
UserModel user, UserProfileMetadata metadata) { UserModel user, UserProfileMetadata metadata) {
if (user != null && user.getServiceAccountClientLink() != null) { if (isServiceAccountUser(user)) {
return new LegacyAttributes(context, attributes, user, metadata, session); return new LegacyAttributes(context, attributes, user, metadata, session);
} }
return new DefaultAttributes(context, attributes, user, metadata, session); return new DefaultAttributes(context, attributes, user, metadata, session);
@ -285,13 +285,14 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE; Predicate<AttributeContext> required = AttributeMetadata.ALWAYS_FALSE;
if (rc != null) { if (rc != null) {
if (rc.isAlways() || context.isRoleForContext(rc.getRoles())) { if (rc.isAlways() || context.isRoleForContext(rc.getRoles())) {
required = AttributeMetadata.ALWAYS_TRUE; // service accounts does not require common attributes
required = c -> !isServiceAccountUser(c.getUser());
// If scopes are configured, we will use scope-based selector and require the attribute just if scope is // If scopes are configured, we will use scope-based selector and require the attribute just if scope is
// in current authenticationSession (either default scope or by 'scope' parameter) // in current authenticationSession (either default scope or by 'scope' parameter)
if (rc.getScopes() != null && !rc.getScopes().isEmpty()) { if (rc.getScopes() != null && !rc.getScopes().isEmpty()) {
if (context.canBeAuthFlowContext()) { if (context.canBeAuthFlowContext()) {
required = (c) -> requestedScopePredicate(c, rc.getScopes()); required = (c) -> !isServiceAccountUser(c.getUser()) && requestedScopePredicate(c, rc.getScopes());
} else { } else {
// Scopes not available for admin and account contexts // Scopes not available for admin and account contexts
required = AttributeMetadata.ALWAYS_FALSE; required = AttributeMetadata.ALWAYS_FALSE;
@ -300,7 +301,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
} else if (context.canBeAuthFlowContext() && rc.getScopes() != null && !rc.getScopes().isEmpty()) { } else if (context.canBeAuthFlowContext() && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
// for contexts executed from auth flow and with configured scopes requirement // for contexts executed from auth flow and with configured scopes requirement
// we have to create required validation with scopes based selector // we have to create required validation with scopes based selector
required = (c) -> requestedScopePredicate(c, rc.getScopes()); required = (c) -> !isServiceAccountUser(c.getUser()) && requestedScopePredicate(c, rc.getScopes());
} }
} }
@ -364,7 +365,7 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
public boolean test(AttributeContext context) { public boolean test(AttributeContext context) {
UserModel user = context.getUser(); UserModel user = context.getUser();
if (user != null && user.getServiceAccountClientLink() != null) { if (isServiceAccountUser(user)) {
return false; return false;
} }
@ -441,6 +442,10 @@ public class DeclarativeUserProfileProvider implements UserProfileProvider {
return ac -> ac.getContext().isRoleForContext(viewRoles) || canEdit.test(ac); return ac -> ac.getContext().isRoleForContext(viewRoles) || canEdit.test(ac);
} }
private boolean isServiceAccountUser(UserModel user) {
return user != null && user.getServiceAccountClientLink() != null;
}
/** /**
* Get parsed config file configured in model. Default one used if not configured. * Get parsed config file configured in model. Default one used if not configured.
*/ */

View file

@ -21,25 +21,33 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThrows;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import jakarta.ws.rs.BadRequestException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.junit.Rule; import java.util.Set;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource; import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.constants.ServiceAccountConstants; import org.keycloak.common.constants.ServiceAccountConstants;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.userprofile.config.UPAttribute;
import org.keycloak.representations.userprofile.config.UPAttributeRequired;
import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.ClientBuilder; import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.RealmBuilder; import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.testsuite.util.UserBuilder; import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.userprofile.UserProfileConstants;
import org.keycloak.validate.validators.EmailValidator;
/** /**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@ -49,13 +57,6 @@ public class ServiceAccountUserProfileTest extends AbstractKeycloakTest {
private static String userId; private static String userId;
private static String userName; private static String userName;
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
@ -158,4 +159,32 @@ public class ServiceAccountUserProfileTest extends AbstractKeycloakTest {
assertFalse(representation.getAttributes().isEmpty()); assertFalse(representation.getAttributes().isEmpty());
assertEquals("attr-1-value", representation.getAttributes().get("attr-1").get(0)); assertEquals("attr-1-value", representation.getAttributes().get("attr-1").get(0));
} }
@Test
public void testEmailFormatIsEnforced() {
final RealmResource realm = adminClient.realm("test");
UserResource serviceAccount = realm.users().get(userId);
UserRepresentation rep = serviceAccount.toRepresentation();
rep.setEmail("invalidEmail");
BadRequestException e = assertThrows(BadRequestException.class, () -> serviceAccount.update(rep));
assertThat(e.getResponse().readEntity(String.class), containsString(EmailValidator.MESSAGE_INVALID_EMAIL));
}
@Test
public void testAttributesAreNotRequired() {
final RealmResource realm = adminClient.realm("test");
UPConfig config = realm.users().userProfile().getConfiguration();
UPAttribute lastName = config.getAttribute(UserModel.LAST_NAME);
assertNotNull("The attribute lastName is not defined in User Profile", lastName);
UPAttributeRequired upRequired = new UPAttributeRequired(Set.of(
UserProfileConstants.ROLE_ADMIN, UserProfileConstants.ROLE_USER), null);
lastName.setRequired(upRequired);
realm.users().userProfile().update(config);
getCleanup().addCleanup(() -> realm.users().userProfile().update(null));
UserResource serviceAccount = realm.users().get(userId);
UserRepresentation rep = serviceAccount.toRepresentation();
rep.setLastName(null);
serviceAccount.update(rep);
}
} }