[KEYCLOAK-17399] - Declarative User Profile and UI
Co-authored-by: Vlastimil Elias <velias@redhat.com>
This commit is contained in:
parent
d2a8a95d79
commit
ef3a0ee06c
91 changed files with 3884 additions and 998 deletions
|
@ -61,7 +61,8 @@ public class Profile {
|
||||||
WEB_AUTHN(Type.DEFAULT, Type.PREVIEW),
|
WEB_AUTHN(Type.DEFAULT, Type.PREVIEW),
|
||||||
CLIENT_POLICIES(Type.DEFAULT),
|
CLIENT_POLICIES(Type.DEFAULT),
|
||||||
CIBA(Type.PREVIEW),
|
CIBA(Type.PREVIEW),
|
||||||
MAP_STORAGE(Type.EXPERIMENTAL);
|
MAP_STORAGE(Type.EXPERIMENTAL),
|
||||||
|
DECLARATIVE_USER_PROFILE(Type.PREVIEW);
|
||||||
|
|
||||||
private final Type typeProject;
|
private final Type typeProject;
|
||||||
private final Type typeProduct;
|
private final Type typeProduct;
|
||||||
|
|
|
@ -21,8 +21,8 @@ public class ProfileTest {
|
||||||
@Test
|
@Test
|
||||||
public void checkDefaultsKeycloak() {
|
public void checkDefaultsKeycloak() {
|
||||||
Assert.assertEquals("community", Profile.getName());
|
Assert.assertEquals("community", Profile.getName());
|
||||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
|
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA);
|
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||||
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||||
|
|
||||||
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
||||||
|
@ -37,8 +37,8 @@ public class ProfileTest {
|
||||||
Profile.init();
|
Profile.init();
|
||||||
|
|
||||||
Assert.assertEquals("product", Profile.getName());
|
Assert.assertEquals("product", Profile.getName());
|
||||||
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE);
|
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.UPLOAD_SCRIPTS, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||||
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA);
|
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.WEB_AUTHN, Profile.Feature.CIBA, Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||||
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
assertEquals(Profile.getDeprecatedFeatures(), Profile.Feature.UPLOAD_SCRIPTS);
|
||||||
|
|
||||||
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
Assert.assertTrue(Profile.Feature.WEB_AUTHN.hasDifferentProductType());
|
||||||
|
|
|
@ -17,16 +17,35 @@
|
||||||
|
|
||||||
package org.keycloak.representations.idm;
|
package org.keycloak.representations.idm;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
*/
|
*/
|
||||||
public class ErrorRepresentation {
|
public class ErrorRepresentation {
|
||||||
|
private String field;
|
||||||
private String errorMessage;
|
private String errorMessage;
|
||||||
private Object[] params;
|
private Object[] params;
|
||||||
|
private List<ErrorRepresentation> errors;
|
||||||
|
|
||||||
public ErrorRepresentation() {
|
public ErrorRepresentation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ErrorRepresentation(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorRepresentation(String field, String errorMessage, Object[] params) {
|
||||||
|
super();
|
||||||
|
this.field = field;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getField() {
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
public String getErrorMessage() {
|
public String getErrorMessage() {
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
@ -42,4 +61,12 @@ public class ErrorRepresentation {
|
||||||
public void setParams(Object[] params) {
|
public void setParams(Object[] params) {
|
||||||
this.params = params;
|
this.params = params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setErrors(List<ErrorRepresentation> errors) {
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ErrorRepresentation> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* 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.admin.client.resource;
|
||||||
|
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*/
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
public interface UserProfileResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
String getConfiguration();
|
||||||
|
|
||||||
|
@PUT
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
Response update(String text);
|
||||||
|
}
|
|
@ -246,6 +246,8 @@ public interface UsersResource {
|
||||||
@Path("{id}")
|
@Path("{id}")
|
||||||
@DELETE
|
@DELETE
|
||||||
Response delete(@PathParam("id") String id);
|
Response delete(@PathParam("id") String id);
|
||||||
|
|
||||||
|
@Path("profile")
|
||||||
|
UserProfileResource userProfile();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,8 @@ public enum EventType {
|
||||||
UPDATE_TOTP_ERROR(true),
|
UPDATE_TOTP_ERROR(true),
|
||||||
VERIFY_EMAIL(true),
|
VERIFY_EMAIL(true),
|
||||||
VERIFY_EMAIL_ERROR(true),
|
VERIFY_EMAIL_ERROR(true),
|
||||||
|
VERIFY_PROFILE(true),
|
||||||
|
VERIFY_PROFILE_ERROR(true),
|
||||||
|
|
||||||
REMOVE_TOTP(true),
|
REMOVE_TOTP(true),
|
||||||
REMOVE_TOTP_ERROR(true),
|
REMOVE_TOTP_ERROR(true),
|
||||||
|
|
|
@ -26,6 +26,6 @@ public enum LoginFormsPages {
|
||||||
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
LOGIN_IDP_LINK_CONFIRM, LOGIN_IDP_LINK_EMAIL,
|
||||||
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
|
OAUTH_GRANT, LOGIN_RESET_PASSWORD, LOGIN_UPDATE_PASSWORD, LOGIN_SELECT_AUTHENTICATOR, REGISTER, INFO, ERROR, ERROR_WEBAUTHN, LOGIN_UPDATE_PROFILE,
|
||||||
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
|
LOGIN_PAGE_EXPIRED, CODE, X509_CONFIRM, SAML_POST_FORM,
|
||||||
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE;
|
LOGIN_OAUTH2_DEVICE_VERIFY_USER_CODE, VERIFY_PROFILE;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,25 +43,26 @@ public final class AttributeMetadata {
|
||||||
|
|
||||||
private final String attributeName;
|
private final String attributeName;
|
||||||
private final Predicate<AttributeContext> selector;
|
private final Predicate<AttributeContext> selector;
|
||||||
private final Predicate<AttributeContext> readOnly;
|
private final Predicate<AttributeContext> writeAllowed;
|
||||||
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
/** Predicate to decide if attribute is required, it is handled as required if predicate is null */
|
||||||
private final Predicate<AttributeContext> required;
|
private final Predicate<AttributeContext> required;
|
||||||
|
private final Predicate<AttributeContext> readAllowed;
|
||||||
private List<AttributeValidatorMetadata> validators;
|
private List<AttributeValidatorMetadata> validators;
|
||||||
private Map<String, Object> annotations;
|
private Map<String, Object> annotations;
|
||||||
|
|
||||||
AttributeMetadata(String attributeName) {
|
AttributeMetadata(String attributeName) {
|
||||||
this(attributeName, ALWAYS_TRUE, ALWAYS_FALSE, ALWAYS_TRUE);
|
this(attributeName, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
AttributeMetadata(String attributeName, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
|
||||||
this(attributeName, ALWAYS_TRUE, readOnly, required);
|
this(attributeName, ALWAYS_TRUE, writeAllowed, required, ALWAYS_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector) {
|
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector) {
|
||||||
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE);
|
this(attributeName, selector, ALWAYS_FALSE, ALWAYS_TRUE, ALWAYS_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
AttributeMetadata(String attributeName, List<String> scopes, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required) {
|
||||||
this(attributeName, context -> {
|
this(attributeName, context -> {
|
||||||
KeycloakSession session = context.getSession();
|
KeycloakSession session = context.getSession();
|
||||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||||
|
@ -81,14 +82,17 @@ public final class AttributeMetadata {
|
||||||
|
|
||||||
return authSession.getClientScopes().stream()
|
return authSession.getClientScopes().stream()
|
||||||
.map(id -> clientScopes.getClientScopeById(realm, id).getName()).anyMatch(scopes::contains);
|
.map(id -> clientScopes.getClientScopeById(realm, id).getName()).anyMatch(scopes::contains);
|
||||||
}, readOnly, required);
|
}, writeAllowed, required, ALWAYS_TRUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
AttributeMetadata(String attributeName, Predicate<AttributeContext> selector, Predicate<AttributeContext> writeAllowed,
|
||||||
|
Predicate<AttributeContext> required,
|
||||||
|
Predicate<AttributeContext> readAllowed) {
|
||||||
this.attributeName = attributeName;
|
this.attributeName = attributeName;
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
this.readOnly = readOnly;
|
this.writeAllowed = writeAllowed;
|
||||||
this.required = required;
|
this.required = required;
|
||||||
|
this.readAllowed = readAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
|
@ -100,10 +104,14 @@ public final class AttributeMetadata {
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isReadOnly(AttributeContext context) {
|
public boolean isReadOnly(AttributeContext context) {
|
||||||
return readOnly.test(context);
|
return !writeAllowed.test(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public boolean canView(AttributeContext context) {
|
||||||
|
return readAllowed.test(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
* Check if attribute is required based on it's predicate, it is handled as required if predicate is null
|
* Check if attribute is required based on it's predicate, it is handled as required if predicate is null
|
||||||
* @param context to evaluate requirement of the attribute from
|
* @param context to evaluate requirement of the attribute from
|
||||||
* @return true if attribute is required in provided context
|
* @return true if attribute is required in provided context
|
||||||
|
@ -140,7 +148,7 @@ public final class AttributeMetadata {
|
||||||
if(this.annotations == null) {
|
if(this.annotations == null) {
|
||||||
this.annotations = new HashMap<>();
|
this.annotations = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.annotations.putAll(annotations);
|
this.annotations.putAll(annotations);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -148,7 +156,7 @@ public final class AttributeMetadata {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AttributeMetadata clone() {
|
public AttributeMetadata clone() {
|
||||||
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, readOnly, required);
|
AttributeMetadata cloned = new AttributeMetadata(attributeName, selector, writeAllowed, required, readAllowed);
|
||||||
// we clone validators list to allow adding or removing validators. Validators
|
// we clone validators list to allow adding or removing validators. Validators
|
||||||
// itself are not cloned as we do not expect them to be reconfigured.
|
// itself are not cloned as we do not expect them to be reconfigured.
|
||||||
if (validators != null) {
|
if (validators != null) {
|
||||||
|
@ -160,4 +168,19 @@ public final class AttributeMetadata {
|
||||||
}
|
}
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || !(o instanceof AttributeMetadata)) return false;
|
||||||
|
|
||||||
|
AttributeMetadata that = (AttributeMetadata) o;
|
||||||
|
|
||||||
|
return that.getName().equals(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return attributeName.hashCode();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,9 @@ import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,4 +110,61 @@ public interface Attributes {
|
||||||
* @return the attributes
|
* @return the attributes
|
||||||
*/
|
*/
|
||||||
Set<Map.Entry<String, List<String>>> attributeSet();
|
Set<Map.Entry<String, List<String>>> attributeSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Returns the metadata associated with the attribute with the given {@code name}.
|
||||||
|
*
|
||||||
|
* <p>The {@link AttributeMetadata} is a copy of the original metadata. The original metadata
|
||||||
|
* keeps immutable.
|
||||||
|
*
|
||||||
|
* @param name the attribute name
|
||||||
|
* @return the metadata
|
||||||
|
*/
|
||||||
|
AttributeMetadata getMetadata(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the attribute with the given {@code name} is required.
|
||||||
|
*
|
||||||
|
* @param name the attribute name
|
||||||
|
* @return {@code true} if the attribute is required. Otherwise, {@code false}.
|
||||||
|
*/
|
||||||
|
boolean isRequired(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to {{@link #getReadable(boolean)}} but with the possibility to add or remove
|
||||||
|
* the root attributes.
|
||||||
|
*
|
||||||
|
* @param includeBuiltin if the root attributes should be included.
|
||||||
|
* @return the attributes with read/write permission.
|
||||||
|
*/
|
||||||
|
default Map<String, List<String>> getReadable(boolean includeBuiltin) {
|
||||||
|
return getReadable().entrySet().stream().filter(entry -> {
|
||||||
|
if (includeBuiltin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !isRootAttribute(entry.getKey());
|
||||||
|
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns only the attributes that have read/write permissions.
|
||||||
|
*
|
||||||
|
* @return the attributes with read/write permission.
|
||||||
|
*/
|
||||||
|
Map<String, List<String>> getReadable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the attribute with the given {@code name} is a root attribute.
|
||||||
|
*
|
||||||
|
* @param name the attribute name
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
default boolean isRootAttribute(String name) {
|
||||||
|
return UserModel.USERNAME.equals(name)
|
||||||
|
|| UserModel.EMAIL.equals(name)
|
||||||
|
|| UserModel.FIRST_NAME.equals(name)
|
||||||
|
|| UserModel.LAST_NAME.equals(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, List<String>> toMap();
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,6 @@ import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import org.keycloak.models.Constants;
|
import org.keycloak.models.Constants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -48,7 +47,7 @@ import org.keycloak.validate.ValidationError;
|
||||||
*
|
*
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public final class DefaultAttributes extends HashMap<String, List<String>> implements Attributes {
|
public class DefaultAttributes extends HashMap<String, List<String>> implements Attributes {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To reference dynamic attributes that can be configured as read-only when setting up the provider.
|
* To reference dynamic attributes that can be configured as read-only when setting up the provider.
|
||||||
|
@ -59,7 +58,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
private final UserProfileContext context;
|
private final UserProfileContext context;
|
||||||
private final KeycloakSession session;
|
private final KeycloakSession session;
|
||||||
private final Map<String, AttributeMetadata> metadataByAttribute;
|
private final Map<String, AttributeMetadata> metadataByAttribute;
|
||||||
private final UserModel user;
|
protected final UserModel user;
|
||||||
|
|
||||||
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
public DefaultAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||||
UserProfileMetadata profileMetadata,
|
UserProfileMetadata profileMetadata,
|
||||||
|
@ -79,10 +78,22 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
private boolean isReadOnlyFromMetadata(String attributeName) {
|
private boolean isReadOnlyFromMetadata(String attributeName) {
|
||||||
AttributeMetadata attributeMetadata = metadataByAttribute.get(attributeName);
|
AttributeMetadata attributeMetadata = metadataByAttribute.get(attributeName);
|
||||||
|
|
||||||
if (attributeMetadata != null && attributeMetadata.isReadOnly(createAttributeContext(attributeName, attributeMetadata))) {
|
if (attributeMetadata == null) {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return attributeMetadata.isReadOnly(createAttributeContext(attributeMetadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRequired(String name) {
|
||||||
|
AttributeMetadata attributeMetadata = metadataByAttribute.get(name);
|
||||||
|
|
||||||
|
if (attributeMetadata == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributeMetadata.isRequired(createAttributeContext(attributeMetadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -95,31 +106,33 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
metadatas.addAll(Optional.ofNullable(this.metadataByAttribute.get(READ_ONLY_ATTRIBUTE_KEY))
|
||||||
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
.map(Collections::singletonList).orElse(Collections.emptyList()));
|
||||||
|
|
||||||
List<ValidationContext> failingValidators = Collections.emptyList();
|
Boolean result = null;
|
||||||
|
|
||||||
for (AttributeMetadata metadata : metadatas) {
|
for (AttributeMetadata metadata : metadatas) {
|
||||||
|
AttributeContext attributeContext = createAttributeContext(attribute, metadata);
|
||||||
|
|
||||||
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
for (AttributeValidatorMetadata validator : metadata.getValidators()) {
|
||||||
ValidationContext vc = validator.validate(createAttributeContext(attribute, metadata));
|
ValidationContext vc = validator.validate(attributeContext);
|
||||||
if (!vc.isValid()) {
|
|
||||||
if (failingValidators.equals(Collections.emptyList())) {
|
|
||||||
failingValidators = new ArrayList<>();
|
|
||||||
}
|
|
||||||
failingValidators.add(vc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (listeners != null) {
|
if (vc.isValid()) {
|
||||||
for (ValidationContext failingValidator : failingValidators) {
|
continue;
|
||||||
for (Consumer<ValidationError> consumer : listeners) {
|
}
|
||||||
for(ValidationError err: failingValidator.getErrors()) {
|
|
||||||
consumer.accept(err);
|
if (result == null) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listeners != null) {
|
||||||
|
for (ValidationError error : vc.getErrors()) {
|
||||||
|
for (Consumer<ValidationError> consumer : listeners) {
|
||||||
|
consumer.accept(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return failingValidators.isEmpty();
|
return result == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -142,12 +155,43 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
return entrySet();
|
return entrySet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AttributeMetadata getMetadata(String name) {
|
||||||
|
AttributeMetadata metadata = metadataByAttribute.get(name);
|
||||||
|
|
||||||
|
if (metadata == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getReadable() {
|
||||||
|
Map<String, List<String>> attributes = new HashMap<>(user.getAttributes());
|
||||||
|
|
||||||
|
if (attributes.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> toMap() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private AttributeContext createAttributeContext(Entry<String, List<String>> attribute, AttributeMetadata metadata) {
|
private AttributeContext createAttributeContext(Entry<String, List<String>> attribute, AttributeMetadata metadata) {
|
||||||
return new AttributeContext(context, session, attribute, user, metadata);
|
return new AttributeContext(context, session, attribute, user, metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AttributeContext createAttributeContext(String attributeName, AttributeMetadata metadata) {
|
private AttributeContext createAttributeContext(String attributeName, AttributeMetadata metadata) {
|
||||||
return createAttributeContext(createAttribute(attributeName), metadata);
|
return new AttributeContext(context, session, createAttribute(attributeName), user, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AttributeContext createAttributeContext(AttributeMetadata metadata) {
|
||||||
|
return createAttributeContext(createAttribute(metadata.getName()), metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, AttributeMetadata> configureMetadata(List<AttributeMetadata> attributes) {
|
private Map<String, AttributeMetadata> configureMetadata(List<AttributeMetadata> attributes) {
|
||||||
|
@ -155,7 +199,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
|
|
||||||
for (AttributeMetadata metadata : attributes) {
|
for (AttributeMetadata metadata : attributes) {
|
||||||
// checks whether the attribute is selected for the current profile
|
// checks whether the attribute is selected for the current profile
|
||||||
if (metadata.isSelected(createAttributeContext(metadata.getName(), metadata))) {
|
if (metadata.isSelected(createAttributeContext(metadata))) {
|
||||||
metadatas.put(metadata.getName(), metadata);
|
metadatas.put(metadata.getName(), metadata);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,9 +234,8 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
Map<String, List<String>> newAttributes = new HashMap<>();
|
Map<String, List<String>> newAttributes = new HashMap<>();
|
||||||
RealmModel realm = session.getContext().getRealm();
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
if (attributes != null && !attributes.isEmpty()) {
|
if (attributes != null) {
|
||||||
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
for (Map.Entry<String, ?> entry : attributes.entrySet()) {
|
||||||
Object value = entry.getValue();
|
|
||||||
String key = entry.getKey();
|
String key = entry.getKey();
|
||||||
|
|
||||||
if (!isSupportedAttribute(key)) {
|
if (!isSupportedAttribute(key)) {
|
||||||
|
@ -204,6 +247,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> values;
|
List<String> values;
|
||||||
|
Object value = entry.getValue();
|
||||||
|
|
||||||
if (value instanceof String) {
|
if (value instanceof String) {
|
||||||
values = Collections.singletonList((String) value);
|
values = Collections.singletonList((String) value);
|
||||||
|
@ -215,26 +259,27 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
values = Collections.singletonList(values.get(0).toLowerCase());
|
values = Collections.singletonList(values.get(0).toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isReadOnlyFromMetadata(key)) {
|
|
||||||
// only revert attribute values if not an internal read-only attribute
|
|
||||||
// for backward compatibility changing these attributes should cause validation errors
|
|
||||||
// ideally, we should just ignore and remove this check
|
|
||||||
if (user == null) {
|
|
||||||
values = EMPTY_VALUE;
|
|
||||||
} else {
|
|
||||||
values = user.getAttributeStream(key).collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newAttributes.put(key, Collections.unmodifiableList(values));
|
newAttributes.put(key, Collections.unmodifiableList(values));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the profile should always hold all attributes defined in the config
|
// the profile should always hold all attributes defined in the config
|
||||||
for (String attributeName : metadataByAttribute.keySet()) {
|
for (String attributeName : metadataByAttribute.keySet()) {
|
||||||
if (isSupportedAttribute(attributeName)) {
|
if (!isSupportedAttribute(attributeName) || newAttributes.containsKey(attributeName)) {
|
||||||
newAttributes.computeIfAbsent(attributeName, s -> EMPTY_VALUE);
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> values = EMPTY_VALUE;
|
||||||
|
AttributeMetadata metadata = metadataByAttribute.get(attributeName);
|
||||||
|
|
||||||
|
// if the attribute is not provided and does not have view permission, use the current values
|
||||||
|
// this check makes possible to decide whether or not validation should happen for read-only attributes
|
||||||
|
// when the context does not have access to such attributes
|
||||||
|
if (user != null && !metadata.canView(createAttributeContext(metadata))) {
|
||||||
|
values = user.getAttributes().get(attributeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
newAttributes.put(attributeName, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
|
@ -287,7 +332,7 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks whether the attribute is a core attribute
|
// checks whether the attribute is a core attribute
|
||||||
return UserModel.USERNAME.equals(name) || UserModel.EMAIL.equals(name) || UserModel.LAST_NAME.equals(name) || UserModel.FIRST_NAME.equals(name);
|
return isRootAttribute(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isReadOnlyInternalAttribute(String attributeName) {
|
private boolean isReadOnlyInternalAttribute(String attributeName) {
|
||||||
|
@ -298,10 +343,10 @@ public final class DefaultAttributes extends HashMap<String, List<String>> imple
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SimpleImmutableEntry<String, List<String>> attribute = createAttribute(attributeName);
|
AttributeContext attributeContext = createAttributeContext(attributeName, readonlyMetadata);
|
||||||
|
|
||||||
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
for (AttributeValidatorMetadata validator : readonlyMetadata.getValidators()) {
|
||||||
ValidationContext vc = validator.validate(createAttributeContext(attribute, readonlyMetadata));
|
ValidationContext vc = validator.validate(attributeContext);
|
||||||
if (!vc.isValid()) {
|
if (!vc.isValid()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ import java.util.function.BiConsumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
|
@ -43,22 +44,24 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
|
|
||||||
private final Function<Attributes, UserModel> userSupplier;
|
private final Function<Attributes, UserModel> userSupplier;
|
||||||
private final Attributes attributes;
|
private final Attributes attributes;
|
||||||
|
private final KeycloakSession session;
|
||||||
private boolean validated;
|
private boolean validated;
|
||||||
private UserModel user;
|
private UserModel user;
|
||||||
|
|
||||||
public DefaultUserProfile(Attributes attributes, Function<Attributes, UserModel> userCreator, UserModel user) {
|
public DefaultUserProfile(Attributes attributes, Function<Attributes, UserModel> userCreator, UserModel user,
|
||||||
|
KeycloakSession session) {
|
||||||
this.userSupplier = userCreator;
|
this.userSupplier = userCreator;
|
||||||
this.attributes = attributes;
|
this.attributes = attributes;
|
||||||
this.user = user;
|
this.user = user;
|
||||||
|
this.session = session;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void validate() {
|
public void validate() {
|
||||||
ValidationException validationException = new ValidationException();
|
ValidationException validationException = new ValidationException(session, user);
|
||||||
|
|
||||||
for (String attributeName : attributes.nameSet()) {
|
for (String attributeName : attributes.nameSet()) {
|
||||||
this.attributes.validate(attributeName,
|
this.attributes.validate(attributeName, validationException);
|
||||||
(error) -> validationException.addError(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationException.hasError()) {
|
if (validationException.hasError()) {
|
||||||
|
@ -121,6 +124,7 @@ public final class DefaultUserProfile implements UserProfile {
|
||||||
// the attribute map was sent.
|
// the attribute map was sent.
|
||||||
if (removeAttributes) {
|
if (removeAttributes) {
|
||||||
Set<String> attrsToRemove = new HashSet<>(user.getAttributes().keySet());
|
Set<String> attrsToRemove = new HashSet<>(user.getAttributes().keySet());
|
||||||
|
|
||||||
attrsToRemove.removeAll(attributes.nameSet());
|
attrsToRemove.removeAll(attributes.nameSet());
|
||||||
|
|
||||||
for (String attr : attrsToRemove) {
|
for (String attr : attrsToRemove) {
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.Validator;
|
import org.keycloak.validate.Validator;
|
||||||
|
|
||||||
|
@ -46,4 +50,12 @@ public class UserProfileAttributeValidationContext extends ValidationContext {
|
||||||
return attributeContext;
|
return attributeContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getAttributes() {
|
||||||
|
Map<String, Object> attributes = super.getAttributes();
|
||||||
|
|
||||||
|
attributes.put(UserModel.class.getName(), getAttributeContext().getUser());
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -19,6 +19,9 @@
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import static org.keycloak.userprofile.AttributeMetadata.ALWAYS_FALSE;
|
||||||
|
import static org.keycloak.userprofile.AttributeMetadata.ALWAYS_TRUE;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -42,10 +45,6 @@ public final class UserProfileMetadata implements Cloneable {
|
||||||
return attributes;
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addAttributes(AttributeMetadata... metadata) {
|
|
||||||
addAttributes(Arrays.asList(metadata));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAttributes(List<AttributeMetadata> metadata) {
|
public void addAttributes(List<AttributeMetadata> metadata) {
|
||||||
if (attributes == null) {
|
if (attributes == null) {
|
||||||
attributes = new ArrayList<>();
|
attributes = new ArrayList<>();
|
||||||
|
@ -62,16 +61,20 @@ public final class UserProfileMetadata implements Cloneable {
|
||||||
return addAttribute(name, Arrays.asList(validator));
|
return addAttribute(name, Arrays.asList(validator));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> readAllowed, AttributeValidatorMetadata... validator) {
|
||||||
|
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, readAllowed).addValidator(Arrays.asList(validator)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AttributeMetadata addAttribute(String name, Predicate<AttributeContext> writeAllowed, List<AttributeValidatorMetadata> validators) {
|
||||||
|
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, ALWAYS_TRUE, ALWAYS_TRUE).addValidator(validators));
|
||||||
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validators) {
|
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validators) {
|
||||||
return addAttribute(new AttributeMetadata(name).addValidator(validators));
|
return addAttribute(new AttributeMetadata(name).addValidator(validators));
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> required) {
|
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> writeAllowed, Predicate<AttributeContext> required, Predicate<AttributeContext> readAllowed) {
|
||||||
return addAttribute(new AttributeMetadata(name, AttributeMetadata.ALWAYS_FALSE, required).addValidator(validator));
|
return addAttribute(new AttributeMetadata(name, ALWAYS_TRUE, writeAllowed, required, readAllowed).addValidator(validator));
|
||||||
}
|
|
||||||
|
|
||||||
public AttributeMetadata addAttribute(String name, List<AttributeValidatorMetadata> validator, Predicate<AttributeContext> readOnly, Predicate<AttributeContext> required) {
|
|
||||||
return addAttribute(new AttributeMetadata(name, readOnly, required).addValidator(validator));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -97,7 +100,7 @@ public final class UserProfileMetadata implements Cloneable {
|
||||||
|
|
||||||
//deeply clone AttributeMetadata so we can modify them (add validators etc)
|
//deeply clone AttributeMetadata so we can modify them (add validators etc)
|
||||||
if (attributes != null) {
|
if (attributes != null) {
|
||||||
metadata.addAttributes(attributes.stream().map((c)-> c.clone()).collect(Collectors.toList()));
|
metadata.addAttributes(attributes.stream().map(AttributeMetadata::clone).collect(Collectors.toList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
|
|
|
@ -26,6 +26,8 @@ import org.keycloak.provider.Spi;
|
||||||
*/
|
*/
|
||||||
public class UserProfileSpi implements Spi {
|
public class UserProfileSpi implements Spi {
|
||||||
|
|
||||||
|
public static final String ID = "userProfile";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isInternal() {
|
public boolean isInternal() {
|
||||||
return true;
|
return true;
|
||||||
|
@ -33,7 +35,7 @@ public class UserProfileSpi implements Spi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return "userProfile";
|
return ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -19,22 +19,39 @@
|
||||||
|
|
||||||
package org.keycloak.userprofile;
|
package org.keycloak.userprofile;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.text.MessageFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
public final class ValidationException extends RuntimeException {
|
public final class ValidationException extends RuntimeException implements Consumer<ValidationError> {
|
||||||
|
|
||||||
private final Map<String, List<Error>> errors = new HashMap<>();
|
private final Map<String, List<Error>> errors = new HashMap<>();
|
||||||
|
private final BiFunction<String, Object[], String> messageFormatter;
|
||||||
|
|
||||||
|
public ValidationException(KeycloakSession session, UserModel user) {
|
||||||
|
this.messageFormatter = new MessageFormatter(session, user);
|
||||||
|
}
|
||||||
|
|
||||||
public List<Error> getErrors() {
|
public List<Error> getErrors() {
|
||||||
return errors.values().stream().reduce(new ArrayList<>(), (l, r) -> {
|
return errors.values().stream().reduce(new ArrayList<>(), (l, r) -> {
|
||||||
|
@ -72,11 +89,16 @@ public final class ValidationException extends RuntimeException {
|
||||||
return errors.values().stream().flatMap(Collection::stream).anyMatch(error -> names.contains(error.getAttribute()));
|
return errors.values().stream().flatMap(Collection::stream).anyMatch(error -> names.contains(error.getAttribute()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void accept(ValidationError error) {
|
||||||
|
addError(error);
|
||||||
|
}
|
||||||
|
|
||||||
void addError(ValidationError error) {
|
void addError(ValidationError error) {
|
||||||
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
List<Error> errors = this.errors.computeIfAbsent(error.getMessage(), (k) -> new ArrayList<>());
|
||||||
errors.add(new Error(error));
|
errors.add(new Error(error, messageFormatter));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ValidationException [errors=" + errors + "]";
|
return "ValidationException [errors=" + errors + "]";
|
||||||
|
@ -87,12 +109,25 @@ public final class ValidationException extends RuntimeException {
|
||||||
return toString();
|
return toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Response.Status getStatusCode() {
|
||||||
|
for (Map.Entry<String, List<Error>> entry : errors.entrySet()) {
|
||||||
|
for (Error error : entry.getValue()) {
|
||||||
|
if (!Response.Status.BAD_REQUEST.equals(error.getStatusCode())) {
|
||||||
|
return error.getStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Response.Status.BAD_REQUEST;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Error implements Serializable {
|
public static class Error implements Serializable {
|
||||||
|
|
||||||
private final ValidationError error;
|
private final ValidationError error;
|
||||||
|
private final BiFunction<String, Object[], String> messageFormatter;
|
||||||
|
|
||||||
public Error(ValidationError error) {
|
public Error(ValidationError error, BiFunction<String, Object[], String> messageFormatter) {
|
||||||
this.error = error;
|
this.error = error;
|
||||||
|
this.messageFormatter = messageFormatter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAttribute() {
|
public String getAttribute() {
|
||||||
|
@ -104,13 +139,48 @@ public final class ValidationException extends RuntimeException {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object[] getMessageParameters() {
|
public Object[] getMessageParameters() {
|
||||||
return error.getMessageParameters();
|
return error.getInputHintWithMessageParameters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Error [error=" + error + "]";
|
return "Error [error=" + error + "]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFormattedMessage() {
|
||||||
|
return messageFormatter.apply(getMessage(), getMessageParameters());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response.Status getStatusCode() {
|
||||||
|
return error.getStatusCode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class MessageFormatter implements BiFunction<String, Object[], String> {
|
||||||
|
|
||||||
|
private final Locale locale;
|
||||||
|
private final Properties messages;
|
||||||
|
|
||||||
|
public MessageFormatter(KeycloakSession session, UserModel user) {
|
||||||
|
try {
|
||||||
|
KeycloakContext context = session.getContext();
|
||||||
|
locale = context.resolveLocale(user);
|
||||||
|
messages = getTheme(session).getMessages(locale);
|
||||||
|
RealmModel realm = context.getRealm();
|
||||||
|
Map<String, String> localizationTexts = realm.getRealmLocalizationTextsByLocale(locale.toLanguageTag());
|
||||||
|
messages.putAll(localizationTexts);
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to configure error messages", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Theme getTheme(KeycloakSession session) throws IOException {
|
||||||
|
return session.theme().getTheme(Theme.Type.ADMIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String apply(String s, Object[] objects) {
|
||||||
|
return new MessageFormat(messages.getProperty(s, s), locale).format(objects);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate;
|
package org.keycloak.validate;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Denotes an error found during validation.
|
* Denotes an error found during validation.
|
||||||
|
@ -60,6 +62,14 @@ public class ValidationError implements Serializable {
|
||||||
*/
|
*/
|
||||||
private final Object[] messageParameters;
|
private final Object[] messageParameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status code associated with this error. This information serves as a hint so that
|
||||||
|
* callers can choose whether they want to respect the status defined for the error.
|
||||||
|
*
|
||||||
|
* TODO: Should be better to refactor {@code Messages} to bing messages to status code as well as any other metadata that might be associated with the message.
|
||||||
|
*/
|
||||||
|
private Response.Status statusCode = Response.Status.BAD_REQUEST;
|
||||||
|
|
||||||
public ValidationError(String validatorId, String inputHint, String message) {
|
public ValidationError(String validatorId, String inputHint, String message) {
|
||||||
this(validatorId, inputHint, message, EMPTY_PARAMETERS);
|
this(validatorId, inputHint, message, EMPTY_PARAMETERS);
|
||||||
}
|
}
|
||||||
|
@ -145,4 +155,13 @@ public class ValidationError implements Serializable {
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "ValidationError{" + "validatorId='" + validatorId + '\'' + ", inputHint='" + inputHint + '\'' + ", message='" + message + '\'' + ", messageParameters=" + Arrays.toString(messageParameters) + '}';
|
return "ValidationError{" + "validatorId='" + validatorId + '\'' + ", inputHint='" + inputHint + '\'' + ", message='" + message + '\'' + ", messageParameters=" + Arrays.toString(messageParameters) + '}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ValidationError setStatusCode(Response.Status statusCode) {
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Response.Status getStatusCode() {
|
||||||
|
return statusCode;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -24,6 +24,7 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.validate.validators.LocalDateValidator;
|
||||||
import org.keycloak.validate.validators.EmailValidator;
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
import org.keycloak.validate.validators.IntegerValidator;
|
import org.keycloak.validate.validators.IntegerValidator;
|
||||||
import org.keycloak.validate.validators.LengthValidator;
|
import org.keycloak.validate.validators.LengthValidator;
|
||||||
|
@ -154,6 +155,10 @@ public class Validators {
|
||||||
return IntegerValidator.INSTANCE;
|
return IntegerValidator.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LocalDateValidator dateValidator() {
|
||||||
|
return LocalDateValidator.INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
public static ValidatorConfigValidator validatorConfigValidator() {
|
public static ValidatorConfigValidator validatorConfigValidator() {
|
||||||
return ValidatorConfigValidator.INSTANCE;
|
return ValidatorConfigValidator.INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.utils.StringUtil;
|
import org.keycloak.utils.StringUtil;
|
||||||
import org.keycloak.validate.AbstractSimpleValidator;
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
@ -33,7 +37,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
public abstract class AbstractNumberValidator extends AbstractSimpleValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
|
public static final String MESSAGE_INVALID_NUMBER = "error-invalid-number";
|
||||||
public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
|
public static final String MESSAGE_NUMBER_OUT_OF_RANGE = "error-number-out-of-range";
|
||||||
|
@ -42,6 +46,24 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||||
public static final String KEY_MAX = "max";
|
public static final String KEY_MAX = "max";
|
||||||
|
|
||||||
private final ValidatorConfig defaultConfig;
|
private final ValidatorConfig defaultConfig;
|
||||||
|
|
||||||
|
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_MIN);
|
||||||
|
property.setLabel("Minimum");
|
||||||
|
property.setHelpText("The minimal allowed value - this config is optional.");
|
||||||
|
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
|
configProperties.add(property);
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_MAX);
|
||||||
|
property.setLabel("Maximum");
|
||||||
|
property.setHelpText("The maximal allowed value - this config is optional.");
|
||||||
|
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
|
configProperties.add(property);
|
||||||
|
}
|
||||||
|
|
||||||
public AbstractNumberValidator() {
|
public AbstractNumberValidator() {
|
||||||
// for reflection
|
// for reflection
|
||||||
|
@ -51,6 +73,10 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||||
public AbstractNumberValidator(ValidatorConfig config) {
|
public AbstractNumberValidator(ValidatorConfig config) {
|
||||||
this.defaultConfig = config;
|
this.defaultConfig = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
protected boolean skipValidation(Object value, ValidatorConfig config) {
|
||||||
|
@ -77,7 +103,7 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (number == null) {
|
if (number == null) {
|
||||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER, value));
|
context.addError(new ValidationError(getId(), inputHint, MESSAGE_INVALID_NUMBER));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,12 +111,12 @@ public abstract class AbstractNumberValidator extends AbstractSimpleValidator {
|
||||||
Number max = getMinMaxConfig(config, KEY_MAX);
|
Number max = getMinMaxConfig(config, KEY_MAX);
|
||||||
|
|
||||||
if (min != null && isFirstGreaterThanToSecond(min, number)) {
|
if (min != null && isFirstGreaterThanToSecond(min, number)) {
|
||||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max != null && isFirstGreaterThanToSecond(number, max)) {
|
if (max != null && isFirstGreaterThanToSecond(number, max)) {
|
||||||
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, value, min, max));
|
context.addError(new ValidationError(getId(), inputHint, MESSAGE_NUMBER_OUT_OF_RANGE, min, max));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
import org.keycloak.validate.ValidatorConfig;
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +25,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public class DoubleValidator extends AbstractNumberValidator {
|
public class DoubleValidator extends AbstractNumberValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final String ID = "double";
|
public static final String ID = "double";
|
||||||
|
|
||||||
|
@ -60,4 +61,10 @@ public class DoubleValidator extends AbstractNumberValidator {
|
||||||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||||
return n1.doubleValue() > n2.doubleValue();
|
return n1.doubleValue() > n2.doubleValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Validator to check Double number format and optionally min and max values";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,12 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.validate.AbstractStringValidator;
|
import org.keycloak.validate.AbstractStringValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -27,7 +31,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
* Email format validation - accepts plain string and collection of strings, for basic behavior like null/blank values
|
* Email format validation - accepts plain string and collection of strings, for basic behavior like null/blank values
|
||||||
* handling and collections support see {@link AbstractStringValidator}.
|
* handling and collections support see {@link AbstractStringValidator}.
|
||||||
*/
|
*/
|
||||||
public class EmailValidator extends AbstractStringValidator {
|
public class EmailValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final String ID = "email";
|
public static final String ID = "email";
|
||||||
|
|
||||||
|
@ -38,9 +42,6 @@ public class EmailValidator extends AbstractStringValidator {
|
||||||
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
// Actually allow same emails like angular. See ValidationTest.testEmailValidation()
|
||||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*");
|
||||||
|
|
||||||
private EmailValidator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
|
@ -52,4 +53,14 @@ public class EmailValidator extends AbstractStringValidator {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_EMAIL, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Email format validator";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
import org.keycloak.validate.ValidatorConfig;
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +26,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
*
|
*
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public class IntegerValidator extends AbstractNumberValidator {
|
public class IntegerValidator extends AbstractNumberValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final String ID = "integer";
|
public static final String ID = "integer";
|
||||||
public static final IntegerValidator INSTANCE = new IntegerValidator();
|
public static final IntegerValidator INSTANCE = new IntegerValidator();
|
||||||
|
@ -60,4 +61,10 @@ public class IntegerValidator extends AbstractNumberValidator {
|
||||||
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
protected boolean isFirstGreaterThanToSecond(Number n1, Number n2) {
|
||||||
return n1.longValue() > n2.longValue();
|
return n1.longValue() > n2.longValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Validator to check Integer number format and optionally min and max values";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,14 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.validate.AbstractStringValidator;
|
import org.keycloak.validate.AbstractStringValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -34,7 +38,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
* <p>
|
* <p>
|
||||||
* Configuration have to be always provided, with at least one of {@link #KEY_MIN} and {@link #KEY_MAX}.
|
* Configuration have to be always provided, with at least one of {@link #KEY_MIN} and {@link #KEY_MAX}.
|
||||||
*/
|
*/
|
||||||
public class LengthValidator extends AbstractStringValidator {
|
public class LengthValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final LengthValidator INSTANCE = new LengthValidator();
|
public static final LengthValidator INSTANCE = new LengthValidator();
|
||||||
|
|
||||||
|
@ -46,7 +50,22 @@ public class LengthValidator extends AbstractStringValidator {
|
||||||
public static final String KEY_MAX = "max";
|
public static final String KEY_MAX = "max";
|
||||||
public static final String KEY_TRIM_DISABLED = "trim-disabled";
|
public static final String KEY_TRIM_DISABLED = "trim-disabled";
|
||||||
|
|
||||||
private LengthValidator() {
|
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_MIN);
|
||||||
|
property.setLabel("Minimum length");
|
||||||
|
property.setHelpText("The minimum length");
|
||||||
|
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
|
configProperties.add(property);
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_MAX);
|
||||||
|
property.setLabel("Maximum length");
|
||||||
|
property.setHelpText("The maximum length");
|
||||||
|
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
|
configProperties.add(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,12 +85,12 @@ public class LengthValidator extends AbstractStringValidator {
|
||||||
int length = value.length();
|
int length = value.length();
|
||||||
|
|
||||||
if (config.containsKey(KEY_MIN) && length < min.intValue()) {
|
if (config.containsKey(KEY_MIN) && length < min.intValue()) {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.containsKey(KEY_MAX) && length > max.intValue()) {
|
if (config.containsKey(KEY_MAX) && length > max.intValue()) {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, value, min, max));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_LENGTH, min, max));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,4 +132,14 @@ public class LengthValidator extends AbstractStringValidator {
|
||||||
}
|
}
|
||||||
return new ValidationResult(errors);
|
return new ValidationResult(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Length validator";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
/*
|
||||||
|
* 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.validate.validators;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
import org.keycloak.validate.AbstractStringValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidationResult;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A date validator that only takes into account the format associated with the current locale.
|
||||||
|
*/
|
||||||
|
public class LocalDateValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
|
public static final LocalDateValidator INSTANCE = new LocalDateValidator();
|
||||||
|
|
||||||
|
public static final String ID = "local-date";
|
||||||
|
|
||||||
|
public static final String MESSAGE_INVALID_DATE = "error-invalid-date";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doValidate(String value, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
UserModel user = (UserModel) context.getAttributes().get(UserModel.class.getName());
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
KeycloakContext keycloakContext = session.getContext();
|
||||||
|
Locale locale = keycloakContext.resolveLocale(user);
|
||||||
|
DateFormat formatter = DateFormat.getDateInstance(DateFormat.SHORT, locale);
|
||||||
|
|
||||||
|
formatter.setLenient(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
formatter.parse(value);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_INVALID_DATE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationResult validateConfig(KeycloakSession session, ValidatorConfig config) {
|
||||||
|
return ValidationResult.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Validates date formats based on the realm or user locale.";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isIgnoreEmptyValuesConfigured(ValidatorConfig config) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,15 +33,12 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
*/
|
*/
|
||||||
public class NotBlankValidator implements SimpleValidator {
|
public class NotBlankValidator implements SimpleValidator {
|
||||||
|
|
||||||
public static final String ID = "blank";
|
public static final String ID = "not-blank";
|
||||||
|
|
||||||
public static final String MESSAGE_BLANK = "error-invalid-blank";
|
public static final String MESSAGE_BLANK = "error-invalid-blank";
|
||||||
|
|
||||||
public static final NotBlankValidator INSTANCE = new NotBlankValidator();
|
public static final NotBlankValidator INSTANCE = new NotBlankValidator();
|
||||||
|
|
||||||
private NotBlankValidator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
|
|
|
@ -38,9 +38,6 @@ public class NotEmptyValidator implements SimpleValidator {
|
||||||
|
|
||||||
public static final String MESSAGE_ERROR_EMPTY = "error-empty";
|
public static final String MESSAGE_ERROR_EMPTY = "error-empty";
|
||||||
|
|
||||||
private NotEmptyValidator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
|
|
|
@ -16,12 +16,16 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.regex.PatternSyntaxException;
|
import java.util.regex.PatternSyntaxException;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.validate.AbstractStringValidator;
|
import org.keycloak.validate.AbstractStringValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -32,7 +36,7 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
* Validate String against configured RegEx pattern - accepts plain string and collection of strings, for basic behavior
|
* Validate String against configured RegEx pattern - accepts plain string and collection of strings, for basic behavior
|
||||||
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
|
* like null/blank values handling and collections support see {@link AbstractStringValidator}.
|
||||||
*/
|
*/
|
||||||
public class PatternValidator extends AbstractStringValidator {
|
public class PatternValidator extends AbstractStringValidator implements ConfiguredProvider {
|
||||||
|
|
||||||
public static final String ID = "pattern";
|
public static final String ID = "pattern";
|
||||||
|
|
||||||
|
@ -41,8 +45,17 @@ public class PatternValidator extends AbstractStringValidator {
|
||||||
public static final String KEY_PATTERN = "pattern";
|
public static final String KEY_PATTERN = "pattern";
|
||||||
|
|
||||||
public static final String MESSAGE_NO_MATCH = "error-pattern-no-match";
|
public static final String MESSAGE_NO_MATCH = "error-pattern-no-match";
|
||||||
|
|
||||||
|
private static final List<ProviderConfigProperty> configProperties = new ArrayList<>();
|
||||||
|
|
||||||
private PatternValidator() {
|
static {
|
||||||
|
ProviderConfigProperty property;
|
||||||
|
property = new ProviderConfigProperty();
|
||||||
|
property.setName(KEY_PATTERN);
|
||||||
|
property.setLabel("RegExp pattern");
|
||||||
|
property.setHelpText("RegExp pattern the value must match. Java Pattern syntax is used.");
|
||||||
|
property.setType(ProviderConfigProperty.STRING_TYPE);
|
||||||
|
configProperties.add(property);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -55,7 +68,7 @@ public class PatternValidator extends AbstractStringValidator {
|
||||||
Pattern pattern = config.getPattern(KEY_PATTERN);
|
Pattern pattern = config.getPattern(KEY_PATTERN);
|
||||||
|
|
||||||
if (!pattern.matcher(value).matches()) {
|
if (!pattern.matcher(value).matches()) {
|
||||||
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, value, config.getString(KEY_PATTERN)));
|
context.addError(new ValidationError(ID, inputHint, MESSAGE_NO_MATCH, config.getString(KEY_PATTERN)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,5 +91,15 @@ public class PatternValidator extends AbstractStringValidator {
|
||||||
}
|
}
|
||||||
return new ValidationResult(errors);
|
return new ValidationResult(errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "RegExp Pattern validator";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return configProperties;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.validate.validators;
|
package org.keycloak.validate.validators;
|
||||||
|
|
||||||
|
import org.keycloak.provider.ConfiguredProvider;
|
||||||
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
import org.keycloak.validate.SimpleValidator;
|
import org.keycloak.validate.SimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
|
@ -28,13 +30,14 @@ import java.net.URL;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
* URI validation - accepts {@link URI}, {@link URL} and single String. Null input is valid, use other validators (like
|
||||||
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
* {@link NotBlankValidator} or {@link NotEmptyValidator} to force field as required.
|
||||||
*/
|
*/
|
||||||
public class UriValidator implements SimpleValidator {
|
public class UriValidator implements SimpleValidator, ConfiguredProvider {
|
||||||
|
|
||||||
public static final UriValidator INSTANCE = new UriValidator();
|
public static final UriValidator INSTANCE = new UriValidator();
|
||||||
|
|
||||||
|
@ -56,9 +59,6 @@ public class UriValidator implements SimpleValidator {
|
||||||
|
|
||||||
public static final String ID = "uri";
|
public static final String ID = "uri";
|
||||||
|
|
||||||
private UriValidator() {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return ID;
|
return ID;
|
||||||
|
@ -136,4 +136,14 @@ public class UriValidator implements SimpleValidator {
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHelpText() {
|
||||||
|
return "Uri Validator";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ProviderConfigProperty> getConfigProperties() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
org.keycloak.validate.validators.LengthValidator
|
||||||
|
org.keycloak.validate.validators.NotEmptyValidator
|
||||||
|
org.keycloak.validate.validators.UriValidator
|
||||||
|
org.keycloak.validate.validators.EmailValidator
|
||||||
|
org.keycloak.validate.validators.NotBlankValidator
|
||||||
|
org.keycloak.validate.validators.PatternValidator
|
||||||
|
org.keycloak.validate.validators.DoubleValidator
|
||||||
|
org.keycloak.validate.validators.IntegerValidator
|
||||||
|
org.keycloak.validate.validators.LocalDateValidator
|
|
@ -87,7 +87,7 @@ public class ValidatorTest {
|
||||||
Assert.assertEquals(LengthValidator.ID, error.getValidatorId());
|
Assert.assertEquals(LengthValidator.ID, error.getValidatorId());
|
||||||
Assert.assertEquals(inputHint, error.getInputHint());
|
Assert.assertEquals(inputHint, error.getInputHint());
|
||||||
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH, error.getMessage());
|
Assert.assertEquals(LengthValidator.MESSAGE_INVALID_LENGTH, error.getMessage());
|
||||||
Assert.assertEquals(input, error.getMessageParameters()[0]);
|
Assert.assertEquals(new Integer(2), error.getMessageParameters()[0]);
|
||||||
|
|
||||||
Assert.assertTrue(result.hasErrorsForValidatorId(LengthValidator.ID));
|
Assert.assertTrue(result.hasErrorsForValidatorId(LengthValidator.ID));
|
||||||
Assert.assertFalse(result.hasErrorsForValidatorId("unknown"));
|
Assert.assertFalse(result.hasErrorsForValidatorId("unknown"));
|
||||||
|
|
|
@ -299,7 +299,8 @@ public interface UserModel extends RoleMapperModel {
|
||||||
void setServiceAccountClientLink(String clientInternalId);
|
void setServiceAccountClientLink(String clientInternalId);
|
||||||
|
|
||||||
enum RequiredAction {
|
enum RequiredAction {
|
||||||
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS
|
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD, TERMS_AND_CONDITIONS,
|
||||||
|
VERIFY_PROFILE
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,165 @@
|
||||||
|
/*
|
||||||
|
* 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.authentication.requiredactions;
|
||||||
|
|
||||||
|
import javax.ws.rs.HttpMethod;
|
||||||
|
import javax.ws.rs.core.MultivaluedHashMap;
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.authentication.DisplayTypeRequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.InitiatedActionSupport;
|
||||||
|
import org.keycloak.authentication.RequiredActionContext;
|
||||||
|
import org.keycloak.authentication.RequiredActionFactory;
|
||||||
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.forms.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.FormMessage;
|
||||||
|
import org.keycloak.services.validation.Validation;
|
||||||
|
import org.keycloak.userprofile.UserProfile;
|
||||||
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class VerifyUserProfile implements RequiredActionProvider, RequiredActionFactory, DisplayTypeRequiredActionFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public InitiatedActionSupport initiatedActionSupport() {
|
||||||
|
return InitiatedActionSupport.SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void evaluateTriggers(RequiredActionContext context) {
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, user);
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
context.getAuthenticationSession().removeRequiredAction(getId());
|
||||||
|
user.removeRequiredAction(getId());
|
||||||
|
} catch (ValidationException e) {
|
||||||
|
context.getAuthenticationSession().addRequiredAction(getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void requiredActionChallenge(RequiredActionContext context) {
|
||||||
|
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, context.getUser());
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
context.success();
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
List<FormMessage> errors = Validation.getFormErrorsFromValidation(ve.getErrors());
|
||||||
|
MultivaluedMap<String, String> parameters;
|
||||||
|
|
||||||
|
if (context.getHttpRequest().getHttpMethod().equalsIgnoreCase(HttpMethod.GET)) {
|
||||||
|
parameters = new MultivaluedHashMap<>();
|
||||||
|
} else {
|
||||||
|
parameters = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.challenge(createResponse(context, profile, parameters, errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void processAction(RequiredActionContext context) {
|
||||||
|
EventBuilder event = context.getEvent();
|
||||||
|
event.event(EventType.VERIFY_PROFILE);
|
||||||
|
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
|
||||||
|
|
||||||
|
if (!context.getRealm().isEditUsernameAllowed()) {
|
||||||
|
formData.putSingle(UserModel.USERNAME, context.getUser().getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfileProvider provider = context.getSession().getProvider(UserProfileProvider.class);
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, formData, context.getUser());
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.update();
|
||||||
|
context.success();
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
List<FormMessage> errors = Validation.getFormErrorsFromValidation(ve.getErrors());
|
||||||
|
context.challenge(createResponse(context, profile, formData, errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider create(KeycloakSession session) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RequiredActionProvider createDisplay(KeycloakSession session, String displayType) {
|
||||||
|
if (displayType == null) return this;
|
||||||
|
if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null;
|
||||||
|
return ConsoleUpdateProfile.SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(Config.Scope config) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postInit(KeycloakSessionFactory factory) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDisplayText() {
|
||||||
|
return "Verify Profile";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return UserModel.RequiredAction.VERIFY_PROFILE.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Response createResponse(RequiredActionContext context, UserProfile profile,
|
||||||
|
MultivaluedMap<String, String> formData, List<FormMessage> errors) {
|
||||||
|
LoginFormsProvider form = context.form();
|
||||||
|
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
form.setErrors(errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return form.setFormData(formData)
|
||||||
|
.createResponse(UserModel.RequiredAction.VERIFY_PROFILE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,6 +40,7 @@ import org.keycloak.forms.login.freemarker.model.SAMLPostFormBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.TotpBean;
|
import org.keycloak.forms.login.freemarker.model.TotpBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.TotpLoginBean;
|
import org.keycloak.forms.login.freemarker.model.TotpLoginBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.UrlBean;
|
import org.keycloak.forms.login.freemarker.model.UrlBean;
|
||||||
|
import org.keycloak.forms.login.freemarker.model.VerifyProfileBean;
|
||||||
import org.keycloak.forms.login.freemarker.model.X509ConfirmBean;
|
import org.keycloak.forms.login.freemarker.model.X509ConfirmBean;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientScopeModel;
|
import org.keycloak.models.ClientScopeModel;
|
||||||
|
@ -159,6 +160,13 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
actionMessage = Messages.VERIFY_EMAIL;
|
actionMessage = Messages.VERIFY_EMAIL;
|
||||||
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
|
page = LoginFormsPages.LOGIN_VERIFY_EMAIL;
|
||||||
break;
|
break;
|
||||||
|
case VERIFY_PROFILE:
|
||||||
|
UpdateProfileContext verifyProfile = new UserUpdateProfileContext(realm, user);
|
||||||
|
this.attributes.put(UPDATE_PROFILE_CONTEXT_ATTR, verifyProfile);
|
||||||
|
|
||||||
|
actionMessage = Messages.UPDATE_PROFILE;
|
||||||
|
page = LoginFormsPages.VERIFY_PROFILE;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
return Response.serverError().build();
|
return Response.serverError().build();
|
||||||
}
|
}
|
||||||
|
@ -238,6 +246,9 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
|
||||||
case SAML_POST_FORM:
|
case SAML_POST_FORM:
|
||||||
attributes.put("samlPost", new SAMLPostFormBean(formData));
|
attributes.put("samlPost", new SAMLPostFormBean(formData));
|
||||||
break;
|
break;
|
||||||
|
case VERIFY_PROFILE:
|
||||||
|
attributes.put("profile", new VerifyProfileBean(user, formData, session));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return processTemplate(theme, Templates.getTemplate(page), locale);
|
return processTemplate(theme, Templates.getTemplate(page), locale);
|
||||||
|
|
|
@ -72,6 +72,8 @@ public class Templates {
|
||||||
return "login-x509-info.ftl";
|
return "login-x509-info.ftl";
|
||||||
case SAML_POST_FORM:
|
case SAML_POST_FORM:
|
||||||
return "saml-post-form.ftl";
|
return "saml-post-form.ftl";
|
||||||
|
case VERIFY_PROFILE:
|
||||||
|
return "verify-profile.ftl";
|
||||||
default:
|
default:
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
package org.keycloak.forms.login.freemarker.model;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.MultivaluedMap;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
|
import org.keycloak.userprofile.UserProfile;
|
||||||
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class VerifyProfileBean {
|
||||||
|
|
||||||
|
private final UserModel user;
|
||||||
|
private final MultivaluedMap<String, String> formData;
|
||||||
|
private final List<Attribute> attributes;
|
||||||
|
private final UserProfile profile;
|
||||||
|
|
||||||
|
public VerifyProfileBean(UserModel user, MultivaluedMap<String, String> formData, KeycloakSession session) {
|
||||||
|
this.user = user;
|
||||||
|
this.formData = formData;
|
||||||
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
this.profile = provider.create(UserProfileContext.UPDATE_PROFILE, user);
|
||||||
|
this.attributes = toAttributes(profile.getAttributes().getReadable());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Attribute> getAttributes() {
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Attribute> getAllAttributes() {
|
||||||
|
return toAttributes(profile.getAttributes().toMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Attribute> toAttributes(Map<String, List<String>> readable) {
|
||||||
|
return readable.keySet().stream()
|
||||||
|
.map(name -> profile.getAttributes().getMetadata(name)).map(Attribute::new)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Attribute implements Comparable<Attribute> {
|
||||||
|
|
||||||
|
private final AttributeMetadata metadata;
|
||||||
|
|
||||||
|
public Attribute(AttributeMetadata metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return metadata.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return formData.getOrDefault(getName(), singletonList(user.getFirstAttribute(getName()))).get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRequired() {
|
||||||
|
return profile.getAttributes().isRequired(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReadOnly() {
|
||||||
|
return profile.getAttributes().isReadOnly(getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getAnnotations() {
|
||||||
|
Map<String, Object> annotations = metadata.getAnnotations();
|
||||||
|
|
||||||
|
if (annotations == null) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
return annotations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(Attribute o) {
|
||||||
|
return getName().compareTo(o.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,7 @@ import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
|
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
@ -42,4 +43,12 @@ public class ErrorResponse {
|
||||||
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
|
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Response errors(List<ErrorRepresentation> s, Response.Status status) {
|
||||||
|
if (s.size() == 1) {
|
||||||
|
return Response.status(status).entity(s.get(0)).type(MediaType.APPLICATION_JSON).build();
|
||||||
|
}
|
||||||
|
ErrorRepresentation error = new ErrorRepresentation();
|
||||||
|
error.setErrors(s);
|
||||||
|
return Response.status(status).entity(error).type(MediaType.APPLICATION_JSON).build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import org.keycloak.representations.account.ClientRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentRepresentation;
|
import org.keycloak.representations.account.ConsentRepresentation;
|
||||||
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
import org.keycloak.representations.account.ConsentScopeRepresentation;
|
||||||
import org.keycloak.representations.account.UserRepresentation;
|
import org.keycloak.representations.account.UserRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.managers.Auth;
|
import org.keycloak.services.managers.Auth;
|
||||||
import org.keycloak.services.managers.UserConsentManager;
|
import org.keycloak.services.managers.UserConsentManager;
|
||||||
|
@ -47,6 +48,7 @@ import org.keycloak.storage.ReadOnlyException;
|
||||||
import org.keycloak.theme.Theme;
|
import org.keycloak.theme.Theme;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
import org.keycloak.userprofile.ValidationException.Error;
|
||||||
import org.keycloak.userprofile.UserProfile;
|
import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@ import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -136,13 +139,11 @@ public class AccountRestService {
|
||||||
rep.setEmail(user.getEmail());
|
rep.setEmail(user.getEmail());
|
||||||
rep.setEmailVerified(user.isEmailVerified());
|
rep.setEmailVerified(user.isEmailVerified());
|
||||||
rep.setEmailVerified(user.isEmailVerified());
|
rep.setEmailVerified(user.isEmailVerified());
|
||||||
Map<String, List<String>> attributes = user.getAttributes();
|
|
||||||
Map<String, List<String>> copiedAttributes = new HashMap<>(attributes);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
copiedAttributes.remove(UserModel.FIRST_NAME);
|
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||||
copiedAttributes.remove(UserModel.LAST_NAME);
|
|
||||||
copiedAttributes.remove(UserModel.EMAIL);
|
rep.setAttributes(profile.getAttributes().getReadable(false));
|
||||||
copiedAttributes.remove(UserModel.USERNAME);
|
|
||||||
rep.setAttributes(copiedAttributes);
|
|
||||||
|
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
@ -167,21 +168,36 @@ public class AccountRestService {
|
||||||
|
|
||||||
return Response.noContent().build();
|
return Response.noContent().build();
|
||||||
} catch (ValidationException pve) {
|
} catch (ValidationException pve) {
|
||||||
if (pve.hasError(Messages.READ_ONLY_USERNAME))
|
List<ErrorRepresentation> errors = new ArrayList<>();
|
||||||
return ErrorResponse.error(Messages.READ_ONLY_USERNAME, Response.Status.BAD_REQUEST);
|
for(Error err: pve.getErrors()) {
|
||||||
if (pve.hasError(Messages.USERNAME_EXISTS))
|
errors.add(new ErrorRepresentation(err.getAttribute(), err.getMessage(), validationErrorParamsToString(err.getMessageParameters())));
|
||||||
return ErrorResponse.exists(Messages.USERNAME_EXISTS);
|
}
|
||||||
if (pve.hasError(Messages.EMAIL_EXISTS))
|
return ErrorResponse.errors(errors, pve.getStatusCode());
|
||||||
return ErrorResponse.exists(Messages.EMAIL_EXISTS);
|
|
||||||
|
|
||||||
// Here should be possibility to somehow return all errors?
|
|
||||||
String firstErrorMessage = pve.getErrors().get(0).getMessage();
|
|
||||||
return ErrorResponse.error(firstErrorMessage, Response.Status.BAD_REQUEST);
|
|
||||||
} catch (ReadOnlyException e) {
|
} catch (ReadOnlyException e) {
|
||||||
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
return ErrorResponse.error(Messages.READ_ONLY_USER, Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] validationErrorParamsToString(Object[] messageParameters) {
|
||||||
|
if(messageParameters == null)
|
||||||
|
return null;
|
||||||
|
String[] ret = new String[messageParameters.length];
|
||||||
|
int i = 0;
|
||||||
|
for(Object p: messageParameters) {
|
||||||
|
if(p != null) {
|
||||||
|
//first parameter is field name, we add replacer code so it is localized in React UI
|
||||||
|
if(i==0) {
|
||||||
|
ret[i++] = "${"+p.toString()+"}";
|
||||||
|
} else {
|
||||||
|
ret[i++] = p.toString();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get session information.
|
* Get session information.
|
||||||
*
|
*
|
||||||
|
|
|
@ -16,34 +16,29 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.admin;
|
package org.keycloak.services.resources.admin;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.PUT;
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
|
||||||
import javax.ws.rs.Produces;
|
import javax.ws.rs.Produces;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import javax.ws.rs.core.Response.Status;
|
|
||||||
|
|
||||||
import org.keycloak.component.ComponentValidationException;
|
import org.keycloak.component.ComponentValidationException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
public class UserProfileResource {
|
public class UserProfileResource {
|
||||||
|
|
||||||
@Context
|
@Context
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
|
|
||||||
protected RealmModel realm;
|
protected RealmModel realm;
|
||||||
private AdminPermissionEvaluator auth;
|
private AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
@ -52,31 +47,24 @@ public class UserProfileResource {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("configuration")
|
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
public String getConfiguration() {
|
public String getConfiguration() {
|
||||||
|
|
||||||
auth.realm().requireViewRealm();
|
auth.realm().requireViewRealm();
|
||||||
|
return session.getProvider(UserProfileProvider.class).getConfiguration();
|
||||||
UserProfileProvider t = session.getProvider(UserProfileProvider.class);
|
|
||||||
return t.getConfiguration();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PUT
|
@PUT
|
||||||
@Path("configuration")
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response updateConfiguration(String text) throws IOException {
|
public Response update(String text) {
|
||||||
|
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
|
|
||||||
UserProfileProvider t = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider t = session.getProvider(UserProfileProvider.class);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
t.setConfiguration(text);
|
t.setConfiguration(text);
|
||||||
} catch (ComponentValidationException e) {
|
} catch (ComponentValidationException e) {
|
||||||
//show validation result containing details about error
|
//show validation result containing details about error
|
||||||
return Response.status(Status.BAD_REQUEST).type(MediaType.TEXT_PLAIN).entity(e.getMessage()).build();
|
return ErrorResponse.error(e.getMessage(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
return Response.ok(t.getConfiguration()).type(MediaType.APPLICATION_JSON).build();
|
||||||
|
|
|
@ -54,6 +54,7 @@ import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.provider.ProviderFactory;
|
import org.keycloak.provider.ProviderFactory;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ErrorRepresentation;
|
||||||
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
import org.keycloak.representations.idm.UserConsentRepresentation;
|
import org.keycloak.representations.idm.UserConsentRepresentation;
|
||||||
|
@ -98,6 +99,7 @@ import javax.ws.rs.core.Response.Status;
|
||||||
import javax.ws.rs.core.UriBuilder;
|
import javax.ws.rs.core.UriBuilder;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.text.MessageFormat;
|
import java.text.MessageFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -171,7 +173,7 @@ public class UserResource {
|
||||||
|
|
||||||
UserProfile profile = session.getProvider(UserProfileProvider.class).create(USER_API, rep.toAttributes(), user);
|
UserProfile profile = session.getProvider(UserProfileProvider.class).create(USER_API, rep.toAttributes(), user);
|
||||||
|
|
||||||
Response response = validateUserProfile(profile);
|
Response response = validateUserProfile(profile, user, session);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -205,18 +207,17 @@ public class UserResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Response validateUserProfile(UserProfile profile) {
|
public static Response validateUserProfile(UserProfile profile, UserModel user, KeycloakSession session) {
|
||||||
try {
|
try {
|
||||||
profile.validate();
|
profile.validate();
|
||||||
} catch (ValidationException pve) {
|
} catch (ValidationException pve) {
|
||||||
|
List<ErrorRepresentation> errors = new ArrayList<>();
|
||||||
|
|
||||||
for (ValidationException.Error error : pve.getErrors()) {
|
for (ValidationException.Error error : pve.getErrors()) {
|
||||||
StringBuilder s = new StringBuilder("Failed to update attribute " + error.getAttribute() + ": ");
|
errors.add(new ErrorRepresentation(error.getFormattedMessage()));
|
||||||
|
|
||||||
s.append(error.getMessage()).append(", ");
|
|
||||||
|
|
||||||
logger.warn(s);
|
|
||||||
}
|
}
|
||||||
return ErrorResponse.error("Could not update user! See server log for more details", Response.Status.BAD_REQUEST);
|
|
||||||
|
return ErrorResponse.errors(errors, Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -281,6 +282,15 @@ public class UserResource {
|
||||||
}
|
}
|
||||||
rep.setAccess(auth.users().getAccess(user));
|
rep.setAccess(auth.users().getAccess(user));
|
||||||
|
|
||||||
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
UserProfile profile = provider.create(USER_API, user);
|
||||||
|
|
||||||
|
Map<String, List<String>> attributes = profile.getAttributes().getReadable(false);
|
||||||
|
|
||||||
|
if (!attributes.isEmpty()) {
|
||||||
|
rep.setAttributes(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -155,7 +155,7 @@ public class UsersResource {
|
||||||
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
UserProfile profile = profileProvider.create(USER_API, rep.toAttributes());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Response response = UserResource.validateUserProfile(profile);
|
Response response = UserResource.validateUserProfile(profile, null, session);
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
@ -385,6 +385,19 @@ public class UsersResource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get representation of the user
|
||||||
|
*
|
||||||
|
* @param id User id
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Path("profile")
|
||||||
|
public UserProfileResource userProfile() {
|
||||||
|
UserProfileResource resource = new UserProfileResource(realm, auth);
|
||||||
|
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
||||||
|
return resource;
|
||||||
|
}
|
||||||
|
|
||||||
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
|
private Stream<UserRepresentation> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
|
||||||
session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts);
|
session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
|
import org.keycloak.userprofile.DefaultAttributes;
|
||||||
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary implementation of {@link org.keycloak.userprofile.Attributes}. It should be removed once
|
||||||
|
* the {@link DeclarativeUserProfileProvider} becomes the default.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class DeclarativeAttributes extends DefaultAttributes {
|
||||||
|
|
||||||
|
public DeclarativeAttributes(UserProfileContext context, Map<String, ?> attributes,
|
||||||
|
UserModel user, UserProfileMetadata profileMetadata,
|
||||||
|
KeycloakSession session) {
|
||||||
|
super(context, attributes, user, profileMetadata, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, List<String>> getReadable() {
|
||||||
|
Map<String, List<String>> attributes = new HashMap<>(this);
|
||||||
|
|
||||||
|
for (String name : nameSet()) {
|
||||||
|
AttributeMetadata metadata = getMetadata(name);
|
||||||
|
|
||||||
|
if (metadata == null || !metadata.canView(createAttributeContext(metadata))) {
|
||||||
|
attributes.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,7 +17,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
|
@ -17,10 +17,11 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
import static org.keycloak.protocol.oidc.TokenManager.getRequestedClientScopes;
|
||||||
|
import static org.keycloak.userprofile.config.UPConfigUtils.readConfig;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -31,30 +32,33 @@ import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
import org.keycloak.common.util.MultivaluedHashMap;
|
import org.keycloak.common.util.MultivaluedHashMap;
|
||||||
import org.keycloak.common.util.StreamUtil;
|
import org.keycloak.common.util.StreamUtil;
|
||||||
import org.keycloak.component.AmphibianProviderFactory;
|
import org.keycloak.component.AmphibianProviderFactory;
|
||||||
import org.keycloak.component.ComponentModel;
|
import org.keycloak.component.ComponentModel;
|
||||||
import org.keycloak.component.ComponentValidationException;
|
import org.keycloak.component.ComponentValidationException;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
import org.keycloak.provider.ProviderConfigProperty;
|
import org.keycloak.provider.ProviderConfigProperty;
|
||||||
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
import org.keycloak.userprofile.AttributeMetadata;
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||||
|
import org.keycloak.userprofile.Attributes;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileMetadata;
|
import org.keycloak.userprofile.UserProfileMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
|
import org.keycloak.userprofile.legacy.AbstractUserProfileProvider;
|
||||||
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
import org.keycloak.userprofile.validator.AttributeRequiredByMetadataValidator;
|
||||||
|
import org.keycloak.userprofile.validator.ImmutableAttributeValidator;
|
||||||
import org.keycloak.validate.AbstractSimpleValidator;
|
import org.keycloak.validate.AbstractSimpleValidator;
|
||||||
import org.keycloak.validate.ValidatorConfig;
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
@ -65,13 +69,28 @@ import org.keycloak.validate.ValidatorConfig;
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
* @author Vlastimil Elias <velias@redhat.com>
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
*/
|
*/
|
||||||
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider> implements AmphibianProviderFactory<DeclarativeUserProfileProvider> {
|
public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<DeclarativeUserProfileProvider>
|
||||||
|
implements AmphibianProviderFactory<DeclarativeUserProfileProvider>, EnvironmentDependentProviderFactory {
|
||||||
|
|
||||||
public static final String ID = "declarative-userprofile-provider";
|
public static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
|
||||||
|
public static final String ID = "declarative-user-profile";
|
||||||
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
|
public static final String UP_PIECES_COUNT_COMPONENT_CONFIG_KEY = "config-pieces-count";
|
||||||
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
|
private static final String PARSED_CONFIG_COMPONENT_KEY = "kc.user.profile.metadata";
|
||||||
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
|
private static final String UP_PIECE_COMPONENT_CONFIG_KEY_BASE = "config-piece-";
|
||||||
private static final String SYSTEM_DEFAULT_CONFIG_RESOURCE = "keycloak-default-user-profile.json";
|
|
||||||
|
private static boolean createRequiredForScopePredicate(AttributeContext context, List<String> requiredScopes) {
|
||||||
|
KeycloakSession session = context.getSession();
|
||||||
|
AuthenticationSessionModel authenticationSession = session.getContext().getAuthenticationSession();
|
||||||
|
|
||||||
|
if (authenticationSession == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String requestedScopesString = authenticationSession.getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
||||||
|
ClientModel client = authenticationSession.getClient();
|
||||||
|
|
||||||
|
return getRequestedClientScopes(requestedScopesString, client).map((csm) -> csm.getName()).anyMatch(requiredScopes::contains);
|
||||||
|
}
|
||||||
|
|
||||||
private String defaultRawConfig;
|
private String defaultRawConfig;
|
||||||
|
|
||||||
|
@ -79,8 +98,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
// for reflection
|
// for reflection
|
||||||
}
|
}
|
||||||
|
|
||||||
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
public DeclarativeUserProfileProvider(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry, String defaultRawConfig) {
|
||||||
super(session, metadataRegistry);
|
super(session, metadataRegistry);
|
||||||
|
this.defaultRawConfig = defaultRawConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -90,7 +110,13 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected DeclarativeUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
protected DeclarativeUserProfileProvider create(KeycloakSession session, Map<UserProfileContext, UserProfileMetadata> metadataRegistry) {
|
||||||
return new DeclarativeUserProfileProvider(session, metadataRegistry);
|
return new DeclarativeUserProfileProvider(session, metadataRegistry, defaultRawConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes,
|
||||||
|
UserModel user, UserProfileMetadata metadata) {
|
||||||
|
return new DeclarativeAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -122,10 +148,10 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
List<String> errors = UPConfigUtils.validate(session, upc);
|
List<String> errors = UPConfigUtils.validate(session, upc);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
throw new ComponentValidationException("UserProfile configuration is invalid: " + errors.toString());
|
throw new ComponentValidationException(errors.toString());
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ComponentValidationException("UserProfile configuration is invalid due to JSON parsing error: " + e.getMessage(), e);
|
throw new ComponentValidationException(e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +228,8 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
UserProfileContext context = metadata.getContext();
|
UserProfileContext context = metadata.getContext();
|
||||||
UPConfig parsedConfig = getParsedConfig(model);
|
UPConfig parsedConfig = getParsedConfig(model);
|
||||||
|
|
||||||
if (parsedConfig == null) {
|
// do not change config for REGISTRATION_USER_CREATION context, everything important is covered thanks to REGISTRATION_PROFILE
|
||||||
|
if (parsedConfig == null || context == UserProfileContext.REGISTRATION_USER_CREATION) {
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,46 +254,58 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
if (rc != null && !(UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName))) {
|
if (rc != null && !(UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName))) {
|
||||||
// do not take requirements from config for username and email as they are
|
// do not take requirements from config for username and email as they are
|
||||||
// driven by business logic from parent!
|
// driven by business logic from parent!
|
||||||
|
|
||||||
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
if (rc.isAlways() || UPConfigUtils.isRoleForContext(context, rc.getRoles())) {
|
||||||
validators.add(createRequiredValidator(attrConfig));
|
|
||||||
required = AttributeMetadata.ALWAYS_TRUE;
|
required = AttributeMetadata.ALWAYS_TRUE;
|
||||||
} else if (UPConfigUtils.canBeAuthFlowContext(context) && rc.getScopes() != null && !rc.getScopes().isEmpty()) {
|
} else if (UPConfigUtils.canBeAuthFlowContext(context) && 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) -> attributePredicateAuthFlowRequestedScope(rc.getScopes());
|
required = (c) -> createRequiredForScopePredicate(c, rc.getScopes());
|
||||||
validators.add(createRequiredValidator(attrConfig));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validators.add(new AttributeValidatorMetadata(AttributeRequiredByMetadataValidator.ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
Predicate<AttributeContext> readOnly = AttributeMetadata.ALWAYS_FALSE;
|
Predicate<AttributeContext> writeAllowed = AttributeMetadata.ALWAYS_FALSE;
|
||||||
|
Predicate<AttributeContext> readAllowed = AttributeMetadata.ALWAYS_FALSE;
|
||||||
UPAttributePermissions permissions = attrConfig.getPermissions();
|
UPAttributePermissions permissions = attrConfig.getPermissions();
|
||||||
|
|
||||||
if (permissions != null) {
|
if (permissions != null) {
|
||||||
List<String> editRoles = permissions.getEdit();
|
List<String> editRoles = permissions.getEdit();
|
||||||
|
|
||||||
if (editRoles != null && !editRoles.isEmpty()) {
|
if (!editRoles.isEmpty()) {
|
||||||
readOnly = ac -> !UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
|
writeAllowed = ac -> UPConfigUtils.isRoleForContext(ac.getContext(), editRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> viewRoles = permissions.getView();
|
||||||
|
|
||||||
|
if (viewRoles.isEmpty()) {
|
||||||
|
readAllowed = writeAllowed;
|
||||||
|
} else {
|
||||||
|
readAllowed = createViewAllowedPredicate(writeAllowed, viewRoles);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> annotations = attrConfig.getAnnotations();
|
Map<String, Object> annotations = attrConfig.getAnnotations();
|
||||||
|
|
||||||
if (UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName)) {
|
if (UserModel.USERNAME.equals(attributeName) || UserModel.EMAIL.equals(attributeName)) {
|
||||||
// add format validators for special attributes which may exist from parent
|
if (permissions == null) {
|
||||||
if (!validators.isEmpty()) {
|
writeAllowed = AttributeMetadata.ALWAYS_TRUE;
|
||||||
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
}
|
||||||
if (atts.isEmpty()) {
|
|
||||||
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
List<AttributeMetadata> atts = decoratedMetadata.getAttribute(attributeName);
|
||||||
// doesn't require it.
|
|
||||||
decoratedMetadata.addAttribute(attributeName, validators, readOnly).addAnnotations(annotations);
|
if (atts.isEmpty()) {
|
||||||
} else {
|
// attribute metadata doesn't exist so we have to add it. We keep it optional as Abstract base
|
||||||
// only add configured validators and annotations if attribute metadata exist
|
// doesn't require it.
|
||||||
atts.stream().forEach(c -> c.addValidator(validators).addAnnotations(annotations));
|
decoratedMetadata.addAttribute(attributeName, writeAllowed, validators).addAnnotations(annotations);
|
||||||
}
|
} else {
|
||||||
|
// only add configured validators and annotations if attribute metadata exist
|
||||||
|
atts.stream().forEach(c -> c.addValidator(validators).addAnnotations(annotations));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
decoratedMetadata.addAttribute(attributeName, validators, readOnly, required).addAnnotations(annotations);
|
// always add validation for imuttable/read-only attributes
|
||||||
|
validators.add(new AttributeValidatorMetadata(ImmutableAttributeValidator.ID));
|
||||||
|
decoratedMetadata.addAttribute(attributeName, validators, writeAllowed, required, readAllowed).addAnnotations(annotations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +313,11 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Predicate<AttributeContext> createViewAllowedPredicate(Predicate<AttributeContext> canEdit,
|
||||||
|
List<String> viewRoles) {
|
||||||
|
return ac -> UPConfigUtils.isRoleForContext(ac.getContext(), viewRoles) || canEdit.test(ac);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
|
@ -302,30 +346,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Predicate to select attributes for Authentication flow cases where requested scopes (including configured Default
|
|
||||||
* client scopes) are compared to set of scopes from user profile configuration.
|
|
||||||
* <p>
|
|
||||||
* This patches problem with some auth flows (eg. register) where authSession.getClientScopes() doesn't work
|
|
||||||
* correctly!
|
|
||||||
*
|
|
||||||
* @param scopesConfigured to match
|
|
||||||
* @return true if at least one requested scope matches at least one configured scope
|
|
||||||
*/
|
|
||||||
private boolean attributePredicateAuthFlowRequestedScope(List<String> scopesConfigured) {
|
|
||||||
// never match out of auth flow
|
|
||||||
if (session.getContext().getAuthenticationSession() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return getAuthFlowRequestedScopeNames().stream().anyMatch(scopesConfigured::contains);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<String> getAuthFlowRequestedScopeNames() {
|
|
||||||
String requestedScopesString = session.getContext().getAuthenticationSession().getClientNote(OIDCLoginProtocol.SCOPE_PARAM);
|
|
||||||
return TokenManager.getRequestedClientScopes(requestedScopesString, session.getContext().getAuthenticationSession().getClient()).map((csm) -> csm.getName()).collect(Collectors.toSet());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get componenet to store our "per realm" configuration into.
|
* Get componenet to store our "per realm" configuration into.
|
||||||
*
|
*
|
||||||
|
@ -337,15 +357,6 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny().orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
return realm.getComponentsStream(realm.getId(), UserProfileProvider.class.getName()).findAny().orElseGet(() -> realm.addComponentModel(new DeclarativeUserProfileModel()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create validator for 'required' validation.
|
|
||||||
*
|
|
||||||
* @return validator metadata to run given validation
|
|
||||||
*/
|
|
||||||
protected AttributeValidatorMetadata createRequiredValidator(UPAttribute attrConfig) {
|
|
||||||
return new AttributeValidatorMetadata(AttributeRequiredByMetadataValidator.ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create validator for validation configured in the user profile config.
|
* Create validator for validation configured in the user profile config.
|
||||||
*
|
*
|
||||||
|
@ -363,7 +374,7 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
|
|
||||||
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
|
int count = model.get(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY, 0);
|
||||||
if (count < 1) {
|
if (count < 1) {
|
||||||
return null;
|
return defaultRawConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
@ -390,4 +401,9 @@ public class DeclarativeUserProfileProvider extends AbstractUserProfileProvider<
|
||||||
}
|
}
|
||||||
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
|
model.getConfig().remove(UP_PIECES_COUNT_COMPONENT_CONFIG_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported() {
|
||||||
|
return Profile.isFeatureEnabled(Profile.Feature.DECLARATIVE_USER_PROFILE);
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
|
@ -14,8 +14,9 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,8 +27,8 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class UPAttributePermissions {
|
public class UPAttributePermissions {
|
||||||
|
|
||||||
private List<String> view;
|
private List<String> view = Collections.emptyList();
|
||||||
private List<String> edit;
|
private List<String> edit = Collections.emptyList();
|
||||||
|
|
||||||
public List<String> getView() {
|
public List<String> getView() {
|
||||||
return view;
|
return view;
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
|
@ -14,7 +14,7 @@
|
||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.userprofile.config;
|
||||||
|
|
||||||
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
import static org.keycloak.common.util.ObjectUtil.isBlank;
|
||||||
|
|
||||||
|
@ -106,7 +106,7 @@ public class UPConfigUtils {
|
||||||
errors.add("Attribute configuration without 'name' is not allowed");
|
errors.add("Attribute configuration without 'name' is not allowed");
|
||||||
} else {
|
} else {
|
||||||
if (attNamesCache.contains(attributeName)) {
|
if (attNamesCache.contains(attributeName)) {
|
||||||
errors.add("Duplicit attribute configuration with 'name':'" + attributeName + "'");
|
errors.add("Attribute configuration already exists with 'name':'" + attributeName + "'");
|
||||||
} else {
|
} else {
|
||||||
attNamesCache.add(attributeName);
|
attNamesCache.add(attributeName);
|
||||||
if(!isValidAttributeName(attributeName)) {
|
if(!isValidAttributeName(attributeName)) {
|
||||||
|
@ -134,7 +134,7 @@ public class UPConfigUtils {
|
||||||
* @param attributeName to validate
|
* @param attributeName to validate
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
static boolean isValidAttributeName(String attributeName) {
|
public static boolean isValidAttributeName(String attributeName) {
|
||||||
return Pattern.matches("[a-zA-Z0-9\\._\\-]+", attributeName);
|
return Pattern.matches("[a-zA-Z0-9\\._\\-]+", attributeName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,14 @@ import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.models.KeycloakContext;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.KeycloakSessionFactory;
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
import org.keycloak.userprofile.AttributeValidatorMetadata;
|
||||||
import org.keycloak.userprofile.Attributes;
|
import org.keycloak.userprofile.Attributes;
|
||||||
import org.keycloak.userprofile.DefaultAttributes;
|
import org.keycloak.userprofile.DefaultAttributes;
|
||||||
|
@ -72,6 +76,13 @@ import org.keycloak.validate.validators.EmailValidator;
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
public abstract class AbstractUserProfileProvider<U extends UserProfileProvider> implements UserProfileProvider, UserProfileProviderFactory<U> {
|
||||||
|
|
||||||
|
private static boolean editUsernameCondition(AttributeContext c) {
|
||||||
|
KeycloakSession session = c.getSession();
|
||||||
|
KeycloakContext context = session.getContext();
|
||||||
|
RealmModel realm = context.getRealm();
|
||||||
|
return realm.isEditUsernameAllowed();
|
||||||
|
}
|
||||||
|
|
||||||
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
public static Pattern getRegexPatternString(String[] builtinReadOnlyAttributes) {
|
||||||
if (builtinReadOnlyAttributes != null) {
|
if (builtinReadOnlyAttributes != null) {
|
||||||
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
List<String> readOnlyAttributes = new ArrayList<>(Arrays.asList(builtinReadOnlyAttributes));
|
||||||
|
@ -133,6 +144,8 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(Config.Scope config) {
|
public void init(Config.Scope config) {
|
||||||
|
// make sure registry is clear in case of re-deploy
|
||||||
|
contextualMetadataRegistry.clear();
|
||||||
Pattern pattern = getRegexPatternString(config.getArray("read-only-attributes"));
|
Pattern pattern = getRegexPatternString(config.getArray("read-only-attributes"));
|
||||||
AttributeValidatorMetadata readOnlyValidator = null;
|
AttributeValidatorMetadata readOnlyValidator = null;
|
||||||
|
|
||||||
|
@ -234,8 +247,13 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
|
|
||||||
private UserProfile createUserProfile(UserProfileContext context, Map<String, ?> attributes, UserModel user) {
|
private UserProfile createUserProfile(UserProfileContext context, Map<String, ?> attributes, UserModel user) {
|
||||||
UserProfileMetadata metadata = configureUserProfile(contextualMetadataRegistry.get(context), session);
|
UserProfileMetadata metadata = configureUserProfile(contextualMetadataRegistry.get(context), session);
|
||||||
Attributes profileAttributes = new DefaultAttributes(context, attributes, user, metadata, session);
|
Attributes profileAttributes = createAttributes(context, attributes, user, metadata);
|
||||||
return new DefaultUserProfile(profileAttributes, createUserFactory(), user);
|
return new DefaultUserProfile(profileAttributes, createUserFactory(), user, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Attributes createAttributes(UserProfileContext context, Map<String, ?> attributes, UserModel user,
|
||||||
|
UserProfileMetadata metadata) {
|
||||||
|
return new DefaultAttributes(context, attributes, user, metadata, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addContextualProfileMetadata(UserProfileMetadata metadata) {
|
private void addContextualProfileMetadata(UserProfileMetadata metadata) {
|
||||||
|
@ -259,9 +277,11 @@ public abstract class AbstractUserProfileProvider<U extends UserProfileProvider>
|
||||||
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
private UserProfileMetadata createDefaultProfile(UserProfileContext context, AttributeValidatorMetadata readOnlyValidator) {
|
||||||
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
UserProfileMetadata metadata = new UserProfileMetadata(context);
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.USERNAME, new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
metadata.addAttribute(UserModel.USERNAME, AbstractUserProfileProvider::editUsernameCondition,
|
||||||
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
|
AbstractUserProfileProvider::editUsernameCondition,
|
||||||
new AttributeValidatorMetadata(UsernameMutationValidator.ID));
|
new AttributeValidatorMetadata(UsernameHasValueValidator.ID),
|
||||||
|
new AttributeValidatorMetadata(DuplicateUsernameValidator.ID),
|
||||||
|
new AttributeValidatorMetadata(UsernameMutationValidator.ID));
|
||||||
|
|
||||||
metadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID,
|
metadata.addAttribute(UserModel.FIRST_NAME, new AttributeValidatorMetadata(BlankAttributeValidator.ID,
|
||||||
BlankAttributeValidator.createConfig(Messages.MISSING_FIRST_NAME)));
|
BlankAttributeValidator.createConfig(Messages.MISSING_FIRST_NAME)));
|
||||||
|
|
|
@ -20,6 +20,7 @@ import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.userprofile.AttributeContext;
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.AttributeMetadata;
|
||||||
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
import org.keycloak.validate.SimpleValidator;
|
import org.keycloak.validate.SimpleValidator;
|
||||||
import org.keycloak.validate.ValidationContext;
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
@ -46,10 +47,14 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
|
||||||
AttributeContext attContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
AttributeContext attContext = UserProfileAttributeValidationContext.from(context).getAttributeContext();
|
||||||
|
AttributeMetadata metadata = attContext.getMetadata();
|
||||||
|
|
||||||
if (!attContext.getMetadata().isRequired(attContext)) {
|
if (!metadata.isRequired(attContext)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.isReadOnly(attContext)) {
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
||||||
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||||
} else {
|
} else {
|
||||||
for (String value : values) {
|
for (String value : values) {
|
||||||
if (value == null || Validation.isBlank(value)) {
|
if (Validation.isBlank(value)) {
|
||||||
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
context.addError(new ValidationError(ID, inputHint, ERROR_USER_ATTRIBUTE_REQUIRED));
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
@ -68,5 +73,4 @@ public class AttributeRequiredByMetadataValidator implements SimpleValidator {
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.userprofile.validator;
|
package org.keycloak.userprofile.validator;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -67,7 +68,8 @@ public class DuplicateEmailValidator implements SimpleValidator {
|
||||||
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
// check for duplicated email
|
// check for duplicated email
|
||||||
if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) {
|
if (userByEmail != null && (user == null || !userByEmail.getId().equals(user.getId()))) {
|
||||||
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS));
|
context.addError(new ValidationError(ID, inputHint, Messages.EMAIL_EXISTS)
|
||||||
|
.setStatusCode(Response.Status.CONFLICT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.userprofile.validator;
|
package org.keycloak.userprofile.validator;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -63,7 +64,8 @@ public class DuplicateUsernameValidator implements SimpleValidator {
|
||||||
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
|
|
||||||
if (user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME)) && (existing != null && !existing.getId().equals(user.getId()))) {
|
if (user != null && !value.equals(user.getFirstAttribute(UserModel.USERNAME)) && (existing != null && !existing.getId().equals(user.getId()))) {
|
||||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||||
|
.setStatusCode(Response.Status.CONFLICT));
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.userprofile.validator;
|
package org.keycloak.userprofile.validator;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -66,7 +67,8 @@ public class EmailExistsAsUsernameValidator implements SimpleValidator {
|
||||||
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
UserModel user = UserProfileAttributeValidationContext.from(context).getAttributeContext().getUser();
|
||||||
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
UserModel userByEmail = session.users().getUserByEmail(realm, value);
|
||||||
if (userByEmail != null && user != null && !userByEmail.getId().equals(user.getId())) {
|
if (userByEmail != null && user != null && !userByEmail.getId().equals(user.getId())) {
|
||||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||||
|
.setStatusCode(Response.Status.CONFLICT));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.validator;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.userprofile.AttributeContext;
|
||||||
|
import org.keycloak.userprofile.UserProfileAttributeValidationContext;
|
||||||
|
import org.keycloak.validate.SimpleValidator;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.ValidationError;
|
||||||
|
import org.keycloak.validate.ValidatorConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A validator that fails when the attribute is marked as read only and its value has changed.
|
||||||
|
*
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ImmutableAttributeValidator implements SimpleValidator {
|
||||||
|
|
||||||
|
public static final String ID = "up-immutable-attribute";
|
||||||
|
|
||||||
|
private static final String DEFAULT_ERROR_MESSAGE = "error-user-attribute-read-only";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ValidationContext validate(Object input, String inputHint, ValidationContext context, ValidatorConfig config) {
|
||||||
|
UserProfileAttributeValidationContext ac = (UserProfileAttributeValidationContext) context;
|
||||||
|
AttributeContext attributeContext = ac.getAttributeContext();
|
||||||
|
|
||||||
|
if (!isReadOnly(attributeContext)) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
UserModel user = attributeContext.getUser();
|
||||||
|
|
||||||
|
if (user == null) {
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> currentValue = user.getAttributeStream(inputHint).collect(Collectors.toList());
|
||||||
|
List<String> values = (List<String>) input;
|
||||||
|
|
||||||
|
if (!(currentValue.containsAll(values) && currentValue.size() == values.size())) {
|
||||||
|
context.addError(new ValidationError(ID, inputHint, DEFAULT_ERROR_MESSAGE));
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isReadOnly(AttributeContext attributeContext) {
|
||||||
|
return attributeContext.getMetadata().isReadOnly(attributeContext);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.userprofile.validator;
|
package org.keycloak.userprofile.validator;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -64,7 +65,8 @@ public class RegistrationUsernameExistsValidator implements SimpleValidator {
|
||||||
|
|
||||||
UserModel existing = session.users().getUserByUsername(realm, value);
|
UserModel existing = session.users().getUserByUsername(realm, value);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS));
|
context.addError(new ValidationError(ID, inputHint, Messages.USERNAME_EXISTS)
|
||||||
|
.setStatusCode(Response.Status.CONFLICT));
|
||||||
}
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
|
|
|
@ -23,4 +23,5 @@ org.keycloak.authentication.requiredactions.TermsAndConditions
|
||||||
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
org.keycloak.authentication.requiredactions.WebAuthnRegisterFactory
|
||||||
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
org.keycloak.authentication.requiredactions.WebAuthnPasswordlessRegisterFactory
|
||||||
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
org.keycloak.authentication.requiredactions.UpdateUserLocaleAction
|
||||||
org.keycloak.authentication.requiredactions.DeleteAccount
|
org.keycloak.authentication.requiredactions.DeleteAccount
|
||||||
|
org.keycloak.authentication.requiredactions.VerifyUserProfile
|
|
@ -1,18 +1,20 @@
|
||||||
#
|
#
|
||||||
# Copyright 2016 Red Hat, Inc. and/or its affiliates
|
# /*
|
||||||
# and other contributors as indicated by the @author tags.
|
# * 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.
|
||||||
|
# */
|
||||||
#
|
#
|
||||||
# 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.
|
|
||||||
#
|
|
||||||
|
|
||||||
org.keycloak.userprofile.legacy.DefaultUserProfileProvider
|
org.keycloak.userprofile.legacy.DefaultUserProfileProvider
|
||||||
|
org.keycloak.userprofile.config.DeclarativeUserProfileProvider
|
|
@ -10,3 +10,4 @@ org.keycloak.userprofile.validator.RegistrationEmailAsUsernameUsernameValueValid
|
||||||
org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator
|
org.keycloak.userprofile.validator.RegistrationUsernameExistsValidator
|
||||||
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator
|
org.keycloak.userprofile.validator.RegistrationEmailAsUsernameEmailValueValidator
|
||||||
org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator
|
org.keycloak.userprofile.validator.BrokeringFederatedUsernameHasValueValidator
|
||||||
|
org.keycloak.userprofile.validator.ImmutableAttributeValidator
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"name": "username"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "firstName",
|
||||||
|
"required": {"roles" : ["user"]},
|
||||||
|
"permissions": {
|
||||||
|
"view": ["admin", "user"],
|
||||||
|
"edit": ["admin", "user"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lastName",
|
||||||
|
"required": {"roles" : ["user"]},
|
||||||
|
"permissions": {
|
||||||
|
"view": ["admin", "user"],
|
||||||
|
"edit": ["admin", "user"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -22,6 +22,9 @@ echo ** Adding spi=userProfile with legacy-user-profile configuration of read-on
|
||||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
|
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:add(properties={},enabled=true)
|
||||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||||
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
/subsystem=keycloak-server/spi=userProfile/provider=legacy-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||||
|
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:add(properties={},enabled=true)
|
||||||
|
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=read-only-attributes,value=[deniedFoo,deniedBar*,deniedSome/thing,deniedsome*thing])
|
||||||
|
/subsystem=keycloak-server/spi=userProfile/provider=declarative-user-profile/:map-put(name=properties,key=admin-read-only-attributes,value=[deniedSomeAdmin])
|
||||||
|
|
||||||
echo ** Do not reuse connections for HttpClientProvider within testsuite **
|
echo ** Do not reuse connections for HttpClientProvider within testsuite **
|
||||||
/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:map-put(name=properties,key=reuse-connections,value=false)
|
/subsystem=keycloak-server/spi=connectionsHttpClient/provider=default/:map-put(name=properties,key=reuse-connections,value=false)
|
||||||
|
|
|
@ -25,3 +25,6 @@ spi.truststore.file.password=secret
|
||||||
|
|
||||||
# http client connection reuse settings
|
# http client connection reuse settings
|
||||||
spi.connections-http-client.default.reuse-connections=false
|
spi.connections-http-client.default.reuse-connections=false
|
||||||
|
|
||||||
|
# user profile provider settings
|
||||||
|
spi.user-profile.provider=${keycloak.userProfile.provider:legacy-user-profile}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
#
|
|
||||||
# /*
|
|
||||||
# * 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.
|
|
||||||
# */
|
|
||||||
#
|
|
||||||
|
|
||||||
org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider
|
|
|
@ -1,18 +0,0 @@
|
||||||
{
|
|
||||||
"attributes": [
|
|
||||||
{
|
|
||||||
"name": "username"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "firstName",
|
|
||||||
"required": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "lastName",
|
|
||||||
"required": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -564,8 +564,12 @@ public class AuthServerTestEnricher {
|
||||||
wasUpdated = true;
|
wasUpdated = true;
|
||||||
}
|
}
|
||||||
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
|
if (event.getTestClass().isAnnotationPresent(SetDefaultProvider.class)) {
|
||||||
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, event.getTestClass().getAnnotation(SetDefaultProvider.class));
|
SetDefaultProvider defaultProvider = event.getTestClass().getAnnotation(SetDefaultProvider.class);
|
||||||
wasUpdated = true;
|
|
||||||
|
if (defaultProvider.beforeEnableFeature()) {
|
||||||
|
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContext, defaultProvider);
|
||||||
|
wasUpdated = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
|
|
|
@ -10,4 +10,27 @@ import java.lang.annotation.Target;
|
||||||
public @interface SetDefaultProvider {
|
public @interface SetDefaultProvider {
|
||||||
String spi();
|
String spi();
|
||||||
String providerId();
|
String providerId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Defines whether the default provider should be set by updating an existing Spi configuration.
|
||||||
|
*
|
||||||
|
* <p>This flag is useful when running the Wildfly distribution and when the server is already configured
|
||||||
|
* with a Spi that should only be updated with the default provider.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the default provider should update an existing Spi configuration. Otherwise, the Spi
|
||||||
|
* configuration will be added with the default provider set.
|
||||||
|
*/
|
||||||
|
boolean onlyUpdateDefault() default false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Defines whether the default provider should be set prior to enabling a feature.
|
||||||
|
*
|
||||||
|
* <p>This flag should be used together with {@link EnableFeature} so that the default provider
|
||||||
|
* is set after enabling a feature. It is useful in case the default provider is not enabled by default,
|
||||||
|
* thus requiring the feature to be enabled first.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the default should be set prior to enabling a feature. Otherwise,
|
||||||
|
* the default provider is only set after enabling a feature.
|
||||||
|
*/
|
||||||
|
boolean beforeEnableFeature() default true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,10 +18,13 @@ import org.keycloak.testsuite.arquillian.annotation.DisableFeature;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.DisableFeatures;
|
import org.keycloak.testsuite.arquillian.annotation.DisableFeatures;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeatures;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
import org.keycloak.testsuite.client.KeycloakTestingClient;
|
||||||
|
import org.keycloak.testsuite.util.SpiProvidersSwitchingUtils;
|
||||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||||
import org.wildfly.extras.creaper.core.online.operations.admin.Administration;
|
import org.wildfly.extras.creaper.core.online.operations.admin.Administration;
|
||||||
|
|
||||||
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.AnnotatedElement;
|
import java.lang.reflect.AnnotatedElement;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
@ -74,12 +77,15 @@ public class KeycloakContainerFeaturesController {
|
||||||
private boolean skipRestart;
|
private boolean skipRestart;
|
||||||
private FeatureAction action;
|
private FeatureAction action;
|
||||||
private boolean onlyForProduct;
|
private boolean onlyForProduct;
|
||||||
|
private final AnnotatedElement annotatedElement;
|
||||||
|
|
||||||
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct) {
|
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct
|
||||||
|
, AnnotatedElement annotatedElement) {
|
||||||
this.feature = feature;
|
this.feature = feature;
|
||||||
this.skipRestart = skipRestart;
|
this.skipRestart = skipRestart;
|
||||||
this.action = action;
|
this.action = action;
|
||||||
this.onlyForProduct = onlyForProduct;
|
this.onlyForProduct = onlyForProduct;
|
||||||
|
this.annotatedElement = annotatedElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertPerformed() {
|
private void assertPerformed() {
|
||||||
|
@ -94,6 +100,18 @@ public class KeycloakContainerFeaturesController {
|
||||||
if ((action == FeatureAction.ENABLE && !ProfileAssume.isFeatureEnabled(feature))
|
if ((action == FeatureAction.ENABLE && !ProfileAssume.isFeatureEnabled(feature))
|
||||||
|| (action == FeatureAction.DISABLE && ProfileAssume.isFeatureEnabled(feature))) {
|
|| (action == FeatureAction.DISABLE && ProfileAssume.isFeatureEnabled(feature))) {
|
||||||
action.accept(testContextInstance.get().getTestingClient(), feature);
|
action.accept(testContextInstance.get().getTestingClient(), feature);
|
||||||
|
SetDefaultProvider setDefaultProvider = annotatedElement.getAnnotation(SetDefaultProvider.class);
|
||||||
|
if (setDefaultProvider != null) {
|
||||||
|
try {
|
||||||
|
if (action == FeatureAction.ENABLE) {
|
||||||
|
SpiProvidersSwitchingUtils.addProviderDefaultValue(suiteContextInstance.get(), setDefaultProvider);
|
||||||
|
} else {
|
||||||
|
SpiProvidersSwitchingUtils.removeProvider(suiteContextInstance.get(), setDefaultProvider);
|
||||||
|
}
|
||||||
|
} catch (Exception cause) {
|
||||||
|
throw new RuntimeException("Failed to (un)set default provider", cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,12 +204,13 @@ public class KeycloakContainerFeaturesController {
|
||||||
|
|
||||||
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(EnableFeature.class))
|
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(EnableFeature.class))
|
||||||
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
||||||
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct()))
|
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct(), annotatedElement))
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
|
|
||||||
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(DisableFeature.class))
|
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(DisableFeature.class))
|
||||||
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
|
||||||
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct()))
|
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct(),
|
||||||
|
annotatedElement))
|
||||||
.collect(Collectors.toSet()));
|
.collect(Collectors.toSet()));
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
|
|
@ -16,6 +16,8 @@ import java.security.KeyManagementException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
@ -47,6 +49,9 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
@Inject
|
@Inject
|
||||||
private Instance<SuiteContext> suiteContext;
|
private Instance<SuiteContext> suiteContext;
|
||||||
|
|
||||||
|
private boolean forceReaugmentation;
|
||||||
|
private List<String> additionalArgs = Collections.emptyList();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<KeycloakQuarkusConfiguration> getConfigurationClass() {
|
public Class<KeycloakQuarkusConfiguration> getConfigurationClass() {
|
||||||
return KeycloakQuarkusConfiguration.class;
|
return KeycloakQuarkusConfiguration.class;
|
||||||
|
@ -120,8 +125,12 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
FileUtils.deleteDirectory(configuration.getProvidersPath().resolve("data").toFile());
|
FileUtils.deleteDirectory(configuration.getProvidersPath().resolve("data").toFile());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (configuration.isReaugmentBeforeStart()) {
|
if (isReaugmentBeforeStart()) {
|
||||||
ProcessBuilder reaugment = new ProcessBuilder("./kc.sh", "config");
|
List<String> commands = new ArrayList<>(Arrays.asList("./kc.sh", "config", "-Dquarkus.http.root-path=/auth"));
|
||||||
|
|
||||||
|
addAdditionalCommands(commands);
|
||||||
|
|
||||||
|
ProcessBuilder reaugment = new ProcessBuilder(commands);
|
||||||
|
|
||||||
reaugment.directory(wrkDir).inheritIO();
|
reaugment.directory(wrkDir).inheritIO();
|
||||||
|
|
||||||
|
@ -136,6 +145,10 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
return builder.start();
|
return builder.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isReaugmentBeforeStart() {
|
||||||
|
return configuration.isReaugmentBeforeStart() || forceReaugmentation;
|
||||||
|
}
|
||||||
|
|
||||||
private String[] getProcessCommands() {
|
private String[] getProcessCommands() {
|
||||||
List<String> commands = new ArrayList<>();
|
List<String> commands = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -158,9 +171,15 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
|
|
||||||
commands.add("--cluster=" + System.getProperty("auth.server.quarkus.cluster.config", "local"));
|
commands.add("--cluster=" + System.getProperty("auth.server.quarkus.cluster.config", "local"));
|
||||||
|
|
||||||
|
addAdditionalCommands(commands);
|
||||||
|
|
||||||
return commands.toArray(new String[commands.size()]);
|
return commands.toArray(new String[commands.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addAdditionalCommands(List<String> commands) {
|
||||||
|
commands.addAll(additionalArgs);
|
||||||
|
}
|
||||||
|
|
||||||
private void waitForReadiness() throws MalformedURLException, LifecycleException {
|
private void waitForReadiness() throws MalformedURLException, LifecycleException {
|
||||||
SuiteContext suiteContext = this.suiteContext.get();
|
SuiteContext suiteContext = this.suiteContext.get();
|
||||||
//TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have
|
//TODO: not sure if the best endpoint but it makes sure that everything is properly initialized. Once we have
|
||||||
|
@ -252,4 +271,14 @@ public class KeycloakQuarkusServerDeployableContainer implements DeployableConta
|
||||||
private long getStartTimeout() {
|
private long getStartTimeout() {
|
||||||
return TimeUnit.SECONDS.toMillis(configuration.getStartupTimeoutInSeconds());
|
return TimeUnit.SECONDS.toMillis(configuration.getStartupTimeoutInSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void forceReAugmentation(String... args) {
|
||||||
|
forceReaugmentation = true;
|
||||||
|
additionalArgs = Arrays.asList(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetConfiguration() {
|
||||||
|
additionalArgs = Collections.emptyList();
|
||||||
|
forceReAugmentation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.pages;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.keycloak.testsuite.auth.page.AccountFields;
|
||||||
|
import org.keycloak.testsuite.util.UIUtils;
|
||||||
|
import org.openqa.selenium.By;
|
||||||
|
import org.openqa.selenium.NoSuchElementException;
|
||||||
|
import org.openqa.selenium.WebElement;
|
||||||
|
import org.openqa.selenium.support.FindBy;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*/
|
||||||
|
public class VerifyProfilePage extends AbstractPage {
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private AccountFields.AccountErrors accountErrors;
|
||||||
|
|
||||||
|
@FindBy(id = "firstName")
|
||||||
|
private WebElement firstNameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "lastName")
|
||||||
|
private WebElement lastNameInput;
|
||||||
|
|
||||||
|
@FindBy(id = "email")
|
||||||
|
private WebElement emailInput;
|
||||||
|
|
||||||
|
@FindBy(id = "department")
|
||||||
|
private WebElement departmentInput;
|
||||||
|
|
||||||
|
|
||||||
|
@FindBy(css = "input[type=\"submit\"]")
|
||||||
|
private WebElement submitButton;
|
||||||
|
|
||||||
|
@FindBy(className = "alert-error")
|
||||||
|
private WebElement loginAlertErrorMessage;
|
||||||
|
|
||||||
|
|
||||||
|
public void update(String firstName, String lastName) {
|
||||||
|
firstNameInput.clear();
|
||||||
|
if (firstName != null) {
|
||||||
|
firstNameInput.sendKeys(firstName);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastNameInput.clear();
|
||||||
|
if (lastName != null) {
|
||||||
|
lastNameInput.sendKeys(lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
submitButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(String firstName, String lastName, String department) {
|
||||||
|
departmentInput.clear();
|
||||||
|
if (department != null) {
|
||||||
|
departmentInput.sendKeys(department);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(firstName, lastName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlertError() {
|
||||||
|
try {
|
||||||
|
return UIUtils.getTextFromElement(loginAlertErrorMessage);
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return firstNameInput.getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return lastNameInput.getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDepartment() {
|
||||||
|
return departmentInput.getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDepartmentEnabled() {
|
||||||
|
return departmentInput.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUsernamePresent() {
|
||||||
|
try {
|
||||||
|
return driver.findElement(By.id("username")).isDisplayed();
|
||||||
|
} catch (NoSuchElementException nse) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDepartmentPresent() {
|
||||||
|
try {
|
||||||
|
isDepartmentEnabled();
|
||||||
|
return true;
|
||||||
|
} catch (NoSuchElementException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return emailInput.getAttribute("value");
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isCurrent() {
|
||||||
|
return PageUtils.getPageTitle(driver).equals("Update Account Information");
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountFields.AccountErrors getInputAccountErrors(){
|
||||||
|
return accountErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void open() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,31 +1,52 @@
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
||||||
|
import org.keycloak.testsuite.arquillian.ContainerInfo;
|
||||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
|
import org.keycloak.testsuite.arquillian.containers.KeycloakQuarkusServerDeployableContainer;
|
||||||
import org.wildfly.extras.creaper.core.online.CliException;
|
import org.wildfly.extras.creaper.core.online.CliException;
|
||||||
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
|
|
||||||
public class SpiProvidersSwitchingUtils {
|
public class SpiProvidersSwitchingUtils {
|
||||||
public static void addProviderDefaultValue(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
public static void addProviderDefaultValue(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
ContainerInfo authServerInfo = suiteContext.getAuthServerInfo();
|
||||||
|
|
||||||
|
if (authServerInfo.isUndertow()) {
|
||||||
System.setProperty("keycloak." + annotation.spi() + ".provider", annotation.providerId());
|
System.setProperty("keycloak." + annotation.spi() + ".provider", annotation.providerId());
|
||||||
|
} else if (authServerInfo.isQuarkus()) {
|
||||||
|
KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) authServerInfo.getArquillianContainer().getDeployableContainer();
|
||||||
|
container.forceReAugmentation("-Dkeycloak." + annotation.spi() + ".provider=" + annotation.providerId());
|
||||||
} else {
|
} else {
|
||||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:add(default-provider=\"" + annotation.providerId() + "\")");
|
|
||||||
|
if (annotation.onlyUpdateDefault()) {
|
||||||
|
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + ":write-attribute(name=default-provider, value=" + annotation.providerId() + ")");
|
||||||
|
} else {
|
||||||
|
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:add(default-provider=\"" + annotation.providerId() + "\")");
|
||||||
|
}
|
||||||
|
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void removeProvider(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
public static void removeProvider(SuiteContext suiteContext, SetDefaultProvider annotation) throws IOException, CliException {
|
||||||
if (suiteContext.getAuthServerInfo().isUndertow()) {
|
ContainerInfo authServerInfo = suiteContext.getAuthServerInfo();
|
||||||
|
|
||||||
|
if (authServerInfo.isUndertow()) {
|
||||||
System.clearProperty("keycloak." + annotation.spi() + ".provider");
|
System.clearProperty("keycloak." + annotation.spi() + ".provider");
|
||||||
|
} else if (authServerInfo.isQuarkus()) {
|
||||||
|
KeycloakQuarkusServerDeployableContainer container = (KeycloakQuarkusServerDeployableContainer) authServerInfo.getArquillianContainer().getDeployableContainer();
|
||||||
|
container.resetConfiguration();
|
||||||
} else {
|
} else {
|
||||||
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
|
||||||
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:remove");
|
if (annotation.onlyUpdateDefault()) {
|
||||||
|
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:undefine-attribute(name=default-provider)");
|
||||||
|
} else {
|
||||||
|
client.execute("/subsystem=keycloak-server/spi=" + annotation.spi() + "/:remove");
|
||||||
|
}
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ public class RequiredActionsTest extends AbstractAuthenticationTest {
|
||||||
|
|
||||||
// Dummy RequiredAction is not registered in the realm and WebAuthn actions
|
// Dummy RequiredAction is not registered in the realm and WebAuthn actions
|
||||||
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
List<RequiredActionProviderSimpleRepresentation> result = authMgmtResource.getUnregisteredRequiredActions();
|
||||||
Assert.assertEquals(3, result.size());
|
Assert.assertEquals(4, result.size());
|
||||||
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
RequiredActionProviderSimpleRepresentation action = result.get(0);
|
||||||
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
Assert.assertEquals(DummyRequiredActionFactory.PROVIDER_ID, action.getProviderId());
|
||||||
Assert.assertEquals("Dummy Action", action.getName());
|
Assert.assertEquals("Dummy Action", action.getName());
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * 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.testsuite.admin.userprofile;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.admin.client.resource.UserProfileResource;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.common.util.StreamUtil;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.admin.AbstractAdminTest;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
|
import org.keycloak.userprofile.UserProfileSpi;
|
||||||
|
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||||
|
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||||
|
beforeEnableFeature = false,
|
||||||
|
onlyUpdateDefault = true
|
||||||
|
)
|
||||||
|
public class UserProfileAdminTest extends AbstractAdminTest {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultConfigIfNoneSet() {
|
||||||
|
String defaultRawConfig;
|
||||||
|
|
||||||
|
try (InputStream is = DeclarativeUserProfileProvider.class.getResourceAsStream(DeclarativeUserProfileProvider.SYSTEM_DEFAULT_CONFIG_RESOURCE)) {
|
||||||
|
defaultRawConfig = StreamUtil.readString(is, Charset.defaultCharset());
|
||||||
|
} catch (IOException cause) {
|
||||||
|
throw new RuntimeException("Failed to load default user profile config file", cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(defaultRawConfig, testRealm().users().userProfile().getConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSetDefaultConfig() throws IOException {
|
||||||
|
String rawConfig = "{\"attributes\": [{\"name\": \"test\"}]}";
|
||||||
|
UserProfileResource userProfile = testRealm().users().userProfile();
|
||||||
|
|
||||||
|
userProfile.update(rawConfig);
|
||||||
|
|
||||||
|
assertEquals(rawConfig, userProfile.getConfiguration());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,260 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2016 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.forms;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
|
import org.keycloak.testsuite.pages.*;
|
||||||
|
import org.keycloak.testsuite.pages.AppPage.RequestType;
|
||||||
|
|
||||||
|
import org.keycloak.testsuite.util.*;
|
||||||
|
import org.keycloak.userprofile.UserProfileSpi;
|
||||||
|
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||||
|
* @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*/
|
||||||
|
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||||
|
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||||
|
beforeEnableFeature = false,
|
||||||
|
onlyUpdateDefault = true
|
||||||
|
)
|
||||||
|
public class RegisterWithUserProfileTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected RegisterPage registerPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected VerifyEmailPage verifyEmailPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AccountUpdateProfilePage accountPage;
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public GreenMailRule greenMail = new GreenMailRule();
|
||||||
|
|
||||||
|
|
||||||
|
private static final String SCOPE_LAST_NAME = "lastName";
|
||||||
|
|
||||||
|
private static ClientRepresentation client_scope_default;
|
||||||
|
private static ClientRepresentation client_scope_optional;
|
||||||
|
|
||||||
|
public static String UP_CONFIG_BASIC_ATTRIBUTES = "{\"name\": \"username\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"email\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
|
||||||
|
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name(SCOPE_LAST_NAME).protocol("openid-connect").build()));
|
||||||
|
client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||||
|
client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_LAST_NAME));
|
||||||
|
client_scope_default.setRedirectUris(Collections.singletonList("https://*"));
|
||||||
|
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||||
|
client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_LAST_NAME));
|
||||||
|
client_scope_optional.setRedirectUris(Collections.singletonList("https://*"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess_lastNameOptional() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "", "registerUserSuccessLastNameOptional@email", "registerUserSuccessLastNameOptional", "password", "password");
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
String userId = events.expectRegister("registerUserSuccessLastNameOptional", "registerUserSuccessLastNameOptional@email").assertEvent().getUserId();
|
||||||
|
assertUserRegistered(userId, "registerUserSuccessLastNameOptional", "registerusersuccesslastnameoptional@email", "firstName", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess_lastNameRequiredForScope_notRequested() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_notRequested@email", "registerUserSuccessLastNameRequiredForScope_notRequested", "password", "password");
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
String userId = events.expectRegister("registerUserSuccessLastNameRequiredForScope_notRequested", "registerUserSuccessLastNameRequiredForScope_notRequested@email").assertEvent().getUserId();
|
||||||
|
assertUserRegistered(userId, "registerUserSuccessLastNameRequiredForScope_notRequested", "registerusersuccesslastnamerequiredforscope_notrequested@email", "firstName", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess_lastNameRequiredForScope_requested() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
oauth.scope(SCOPE_LAST_NAME).clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_requested@email", "registerUserSuccessLastNameRequiredForScope_requested", "password", "password");
|
||||||
|
|
||||||
|
//error reported
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
assertEquals("Please specify this field.", registerPage.getInputAccountErrors().getLastNameError());
|
||||||
|
|
||||||
|
//submit correct form
|
||||||
|
registerPage.register("firstName", "lastName", "registerUserSuccessLastNameRequiredForScope_requested@email", "registerUserSuccessLastNameRequiredForScope_requested", "password", "password");
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess_lastNameRequiredForScope_clientDefault() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_LAST_NAME+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "", "registerUserSuccessLastNameRequiredForScope_clientDefault@email", "registerUserSuccessLastNameRequiredForScope_clientDefault", "password", "password");
|
||||||
|
|
||||||
|
//error reported
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
assertEquals("Please specify this field.", registerPage.getInputAccountErrors().getLastNameError());
|
||||||
|
|
||||||
|
//submit correct form
|
||||||
|
registerPage.register("firstName", "lastName", "registerUserSuccessLastNameRequiredForScope_clientDefault@email", "registerUserSuccessLastNameRequiredForScope_clientDefault", "password", "password");
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserSuccess_lastNameLengthValidation() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "last", "registerUserSuccessLastNameLengthValidation@email", "registerUserSuccessLastNameLengthValidation", "password", "password");
|
||||||
|
|
||||||
|
appPage.assertCurrent();
|
||||||
|
assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
|
||||||
|
String userId = events.expectRegister("registerUserSuccessLastNameLengthValidation", "registerUserSuccessLastNameLengthValidation@email").assertEvent().getUserId();
|
||||||
|
assertUserRegistered(userId, "registerUserSuccessLastNameLengthValidation", "registerusersuccesslastnamelengthvalidation@email", "firstName", "last");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void registerUserInvalidLastNameLength() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ UP_CONFIG_BASIC_ATTRIBUTES
|
||||||
|
+ "{\"name\": \"firstName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + VerifyProfileTest.PERMISSIONS_ALL + ", " + VerifyProfileTest.VALIDATIONS_LENGTH + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.clickRegister();
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
|
||||||
|
registerPage.register("firstName", "L", "registerUserInvalidLastNameLength@email", "registerUserInvalidLastNameLength", "password", "password");
|
||||||
|
|
||||||
|
registerPage.assertCurrent();
|
||||||
|
assertEquals("Length must be between 3 and 255.", registerPage.getInputAccountErrors().getLastNameError());
|
||||||
|
|
||||||
|
events.expectRegister("registeruserinvalidlastnamelength", "registerUserInvalidLastNameLength@email")
|
||||||
|
.error("invalid_registration").assertEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertUserRegistered(String userId, String username, String email, String firstName, String lastName) {
|
||||||
|
events.expectLogin().detail("username", username.toLowerCase()).user(userId).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(userId);
|
||||||
|
Assert.assertNotNull(user);
|
||||||
|
Assert.assertNotNull(user.getCreatedTimestamp());
|
||||||
|
// test that timestamp is current with 10s tollerance
|
||||||
|
Assert.assertTrue((System.currentTimeMillis() - user.getCreatedTimestamp()) < 10000);
|
||||||
|
// test user info is set from form
|
||||||
|
assertEquals(username.toLowerCase(), user.getUsername());
|
||||||
|
assertEquals(email.toLowerCase(), user.getEmail());
|
||||||
|
assertEquals(firstName, user.getFirstName());
|
||||||
|
assertEquals(lastName, user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRepresentation getUser(String userId) {
|
||||||
|
return testRealm().users().get(userId).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUserProfileConfiguration(String configuration) {
|
||||||
|
Response r = testRealm().users().userProfile().update(configuration);
|
||||||
|
if (r.getStatus() != 200) {
|
||||||
|
Assert.fail("Configuration not set due to error: " + r.readEntity(String.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,600 @@
|
||||||
|
/*
|
||||||
|
* 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.testsuite.forms;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.jboss.arquillian.graphene.page.Page;
|
||||||
|
import org.jboss.arquillian.test.api.ArquillianResource;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RequiredActionProviderRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
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;
|
||||||
|
import org.keycloak.testsuite.pages.VerifyProfilePage;
|
||||||
|
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||||
|
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.config.DeclarativeUserProfileProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Vlastimil Elias <velias@redhat.com>
|
||||||
|
*/
|
||||||
|
@EnableFeature(value = Profile.Feature.DECLARATIVE_USER_PROFILE, skipRestart = false)
|
||||||
|
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||||
|
beforeEnableFeature = false,
|
||||||
|
onlyUpdateDefault = true)
|
||||||
|
public class VerifyProfileTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
|
private static final String SCOPE_DEPARTMENT = "department";
|
||||||
|
private static final String ATTRIBUTE_DEPARTMENT = "department";
|
||||||
|
|
||||||
|
public static String PERMISSIONS_ALL = "\"permissions\": {\"view\": [\"admin\", \"user\"], \"edit\": [\"admin\", \"user\"]}";
|
||||||
|
public static String PERMISSIONS_ADMIN_ONLY = "\"permissions\": {\"view\": [\"admin\"], \"edit\": [\"admin\"]}";
|
||||||
|
public static String PERMISSIONS_ADMIN_EDITABLE = "\"permissions\": {\"view\": [\"admin\", \"user\"], \"edit\": [\"admin\"]}";
|
||||||
|
|
||||||
|
public static String VALIDATIONS_LENGTH = "\"validations\": {\"length\": { \"min\": 3, \"max\": 255 }}";
|
||||||
|
|
||||||
|
private static final String CONFIGURATION_FOR_USER_EDIT = "{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + "}"
|
||||||
|
+ "]}";
|
||||||
|
|
||||||
|
|
||||||
|
private static String userId;
|
||||||
|
|
||||||
|
private static String user2Id;
|
||||||
|
|
||||||
|
private static String user3Id;
|
||||||
|
|
||||||
|
private static String user4Id;
|
||||||
|
|
||||||
|
private static String user5Id;
|
||||||
|
|
||||||
|
private static ClientRepresentation client_scope_default;
|
||||||
|
private static ClientRepresentation client_scope_optional;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
UserRepresentation user = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test").email("login@test.com").enabled(true).password("password").build();
|
||||||
|
userId = user.getId();
|
||||||
|
|
||||||
|
UserRepresentation user2 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test2").email("login2@test.com").enabled(true).password("password").build();
|
||||||
|
user2Id = user2.getId();
|
||||||
|
|
||||||
|
UserRepresentation user3 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test3").email("login3@test.com").enabled(true).password("password").lastName("ExistingLast").build();
|
||||||
|
user3Id = user3.getId();
|
||||||
|
|
||||||
|
UserRepresentation user4 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test4").email("login4@test.com").enabled(true).password("password").lastName("ExistingLast").build();
|
||||||
|
user4Id = user4.getId();
|
||||||
|
|
||||||
|
UserRepresentation user5 = UserBuilder.create().id(UUID.randomUUID().toString()).username("login-test5").email("login5@test.com").enabled(true).password("password").firstName("ExistingFirst").lastName("ExistingLast").build();
|
||||||
|
user5Id = user5.getId();
|
||||||
|
|
||||||
|
RealmBuilder.edit(testRealm).user(user).user(user2).user(user3).user(user4).user(user5);
|
||||||
|
|
||||||
|
RequiredActionProviderRepresentation action = new RequiredActionProviderRepresentation();
|
||||||
|
action.setAlias(UserModel.RequiredAction.VERIFY_PROFILE.name());
|
||||||
|
action.setProviderId(UserModel.RequiredAction.VERIFY_PROFILE.name());
|
||||||
|
action.setEnabled(true);
|
||||||
|
action.setDefaultAction(false);
|
||||||
|
action.setPriority(10);
|
||||||
|
|
||||||
|
List<RequiredActionProviderRepresentation> actions = new ArrayList<>();
|
||||||
|
actions.add(action);
|
||||||
|
testRealm.setRequiredActions(actions);
|
||||||
|
|
||||||
|
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name(SCOPE_DEPARTMENT).protocol("openid-connect").build()));
|
||||||
|
client_scope_default = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||||
|
client_scope_default.setDefaultClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
|
||||||
|
client_scope_default.setRedirectUris(Collections.singletonList("https://*"));
|
||||||
|
client_scope_optional = KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||||
|
client_scope_optional.setOptionalClientScopes(Collections.singletonList(SCOPE_DEPARTMENT));
|
||||||
|
client_scope_optional.setRedirectUris(Collections.singletonList("https://*"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public AssertEvents events = new AssertEvents(this);
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected AppPage appPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected LoginPage loginPage;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
protected VerifyProfilePage verifyProfilePage;
|
||||||
|
|
||||||
|
@ArquillianResource
|
||||||
|
protected OAuthClient oauth;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultProfile() {
|
||||||
|
setUserProfileConfiguration(null);
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
Assert.assertFalse(verifyProfilePage.isDepartmentPresent());
|
||||||
|
verifyProfilePage.update("First", " ");
|
||||||
|
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
Assert.assertFalse(verifyProfilePage.isDepartmentPresent());
|
||||||
|
verifyProfilePage.update("First", "Last");
|
||||||
|
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(userId).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(userId);
|
||||||
|
assertEquals("First", user.getFirstName());
|
||||||
|
assertEquals("Last", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testUsernameOnlyIfEditAllowed() {
|
||||||
|
RealmRepresentation realm = testRealm().toRepresentation();
|
||||||
|
|
||||||
|
try {
|
||||||
|
setUserProfileConfiguration(null);
|
||||||
|
|
||||||
|
realm.setEditUsernameAllowed(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test", "password");
|
||||||
|
|
||||||
|
assertFalse(verifyProfilePage.isUsernamePresent());
|
||||||
|
|
||||||
|
realm.setEditUsernameAllowed(true);
|
||||||
|
testRealm().update(realm);
|
||||||
|
|
||||||
|
driver.navigate().refresh();
|
||||||
|
assertTrue(verifyProfilePage.isUsernamePresent());
|
||||||
|
} finally {
|
||||||
|
realm.setEditUsernameAllowed(false);
|
||||||
|
testRealm().update(realm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOptionalAttribute() {
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test2", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
verifyProfilePage.update("First", "");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user2Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user2Id);
|
||||||
|
assertEquals("First", user.getFirstName());
|
||||||
|
assertEquals("", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomValidationLastName() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||||
|
updateUser(user5Id, "ExistingFirst", "La", "Department");
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL +","+VALIDATIONS_LENGTH + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.update("First", "L");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.update("First", "Last");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("First", user.getFirstName());
|
||||||
|
assertEquals("Last", user.getLastName());
|
||||||
|
//check that not configured attribute is unchanged
|
||||||
|
assertEquals("Department", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoActionIfNoValidationError() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", "Department");
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL +","+VALIDATIONS_LENGTH + "}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredReadOnlyAttribute() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_EDITABLE + ", \"required\":{}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test3", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());
|
||||||
|
Assert.assertFalse(verifyProfilePage.isDepartmentEnabled());
|
||||||
|
|
||||||
|
//update of the other attributes must be successful in this case
|
||||||
|
verifyProfilePage.update("First", "Last");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user3Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user3Id);
|
||||||
|
assertEquals("First", user.getFirstName());
|
||||||
|
assertEquals("Last", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttributeNotVisible() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ADMIN_ONLY + ", \"required\":{}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test4", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
Assert.assertEquals("ExistingLast", verifyProfilePage.getLastName());
|
||||||
|
Assert.assertFalse("'department' field is visible" , verifyProfilePage.isDepartmentPresent());
|
||||||
|
|
||||||
|
//update of the other attributes must be successful in this case
|
||||||
|
verifyProfilePage.update("First", "Last");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user4Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user4Id);
|
||||||
|
assertEquals("First", user.getFirstName());
|
||||||
|
assertEquals("Last", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredAttribute() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", " ");
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||||
|
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("FirstCC", user.getFirstName());
|
||||||
|
assertEquals("LastCC", user.getLastName());
|
||||||
|
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredOnlyIfUser() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"roles\":[\"user\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||||
|
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", " ");
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||||
|
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("FirstCC", user.getFirstName());
|
||||||
|
assertEquals("LastCC", user.getLastName());
|
||||||
|
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttributeNotRequiredWhenMissingScope() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\"profile\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||||
|
|
||||||
|
oauth.clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("ExistingFirst", user.getFirstName());
|
||||||
|
assertEquals("ExistingLast", user.getLastName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttributeRequiredForScope() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||||
|
|
||||||
|
oauth.scope(SCOPE_DEPARTMENT).clientId(client_scope_optional.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
verifyProfilePage.update("FirstAA", "LastAA", "DepartmentAA");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_optional).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("FirstAA", user.getFirstName());
|
||||||
|
assertEquals("LastAA", user.getLastName());
|
||||||
|
assertEquals("DepartmentAA", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testAttributeRequiredForDefaultScope() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", null);
|
||||||
|
|
||||||
|
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.update("FirstBB", "LastBB", " ");
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.update("FirstBB", "LastBB", "DepartmentBB");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).client(client_scope_default).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("FirstBB", user.getFirstName());
|
||||||
|
assertEquals("LastBB", user.getLastName());
|
||||||
|
assertEquals("DepartmentBB", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoActionIfValidForScope() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", \"required\":{\"scopes\":[\""+SCOPE_DEPARTMENT+"\"]}}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", "ExistingDepartment");
|
||||||
|
|
||||||
|
oauth.clientId(client_scope_default.getClientId()).openLoginForm();
|
||||||
|
|
||||||
|
loginPage.assertCurrent();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("ExistingFirst", user.getFirstName());
|
||||||
|
assertEquals("ExistingLast", user.getLastName());
|
||||||
|
assertEquals("ExistingDepartment", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomValidationInCustomAttribute() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", "D");
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", "+VALIDATIONS_LENGTH+"}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit with error
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", "De");
|
||||||
|
verifyProfilePage.assertCurrent();
|
||||||
|
|
||||||
|
//submit OK
|
||||||
|
verifyProfilePage.update("FirstCC", "LastCC", "DepartmentCC");
|
||||||
|
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
|
||||||
|
events.expectRequiredAction(EventType.VERIFY_PROFILE).user(user5Id).assertEvent();
|
||||||
|
|
||||||
|
UserRepresentation user = getUser(user5Id);
|
||||||
|
assertEquals("FirstCC", user.getFirstName());
|
||||||
|
assertEquals("LastCC", user.getLastName());
|
||||||
|
assertEquals("DepartmentCC", user.firstAttribute(ATTRIBUTE_DEPARTMENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoActionIfSuccessfulValidationForCustomAttribute() {
|
||||||
|
|
||||||
|
setUserProfileConfiguration(CONFIGURATION_FOR_USER_EDIT);
|
||||||
|
updateUser(user5Id, "ExistingFirst", "ExistingLast", "Department");
|
||||||
|
|
||||||
|
setUserProfileConfiguration("{\"attributes\": ["
|
||||||
|
+ "{\"name\": \"firstName\"," + PERMISSIONS_ALL + ", \"required\": {}},"
|
||||||
|
+ "{\"name\": \"lastName\"," + PERMISSIONS_ALL + "},"
|
||||||
|
+ "{\"name\": \"department\"," + PERMISSIONS_ALL + ", "+VALIDATIONS_LENGTH+"}"
|
||||||
|
+ "]}");
|
||||||
|
|
||||||
|
loginPage.open();
|
||||||
|
loginPage.login("login-test5", "password");
|
||||||
|
|
||||||
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
Assert.assertNotNull(oauth.getCurrentQuery().get(OAuth2Constants.CODE));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected UserRepresentation getUser(String userId) {
|
||||||
|
return testRealm().users().get(userId).toRepresentation();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateUser(String userId, String firstName, String lastName, String department) {
|
||||||
|
UserRepresentation ur = testRealm().users().get(userId).toRepresentation();
|
||||||
|
ur.setFirstName(firstName);
|
||||||
|
ur.setLastName(lastName);
|
||||||
|
ur.singleAttribute(ATTRIBUTE_DEPARTMENT, department);
|
||||||
|
testRealm().users().get(userId).update(ur);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void setUserProfileConfiguration(String configuration) {
|
||||||
|
Response r = testRealm().users().userProfile().update(configuration);
|
||||||
|
if (r.getStatus() != 200) {
|
||||||
|
Assert.fail("Configuration not set due to error: " + r.readEntity(String.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider;
|
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +39,6 @@ import org.keycloak.userprofile.UserProfileProvider;
|
||||||
public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakTest {
|
public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
|
||||||
protected static void configureAuthenticationSession(KeycloakSession session) {
|
protected static void configureAuthenticationSession(KeycloakSession session) {
|
||||||
configureSessionRealm(session);
|
|
||||||
Set<String> scopes = new HashSet<>();
|
Set<String> scopes = new HashSet<>();
|
||||||
|
|
||||||
scopes.add("customer");
|
scopes.add("customer");
|
||||||
|
@ -53,16 +52,12 @@ public abstract class AbstractUserProfileTest extends AbstractTestRealmKeycloakT
|
||||||
session.getContext().setAuthenticationSession(createAuthenticationSession(realm.getClientByClientId(clientId), requestedScopes));
|
session.getContext().setAuthenticationSession(createAuthenticationSession(realm.getClientByClientId(clientId), requestedScopes));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static RealmModel configureSessionRealm(KeycloakSession session) {
|
|
||||||
RealmModel realm = session.realms().getRealm(TEST_REALM_NAME);
|
|
||||||
|
|
||||||
session.getContext().setRealm(realm);
|
|
||||||
|
|
||||||
return realm;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static DeclarativeUserProfileProvider getDynamicUserProfileProvider(KeycloakSession session) {
|
protected static DeclarativeUserProfileProvider getDynamicUserProfileProvider(KeycloakSession session) {
|
||||||
return (DeclarativeUserProfileProvider) session.getProvider(UserProfileProvider.class, DeclarativeUserProfileProvider.ID);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
|
||||||
|
provider.setConfiguration(null);
|
||||||
|
|
||||||
|
return (DeclarativeUserProfileProvider) provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static AuthenticationSessionModel createAuthenticationSession(ClientModel client, Set<String> scopes) {
|
protected static AuthenticationSessionModel createAuthenticationSession(ClientModel client, Set<String> scopes) {
|
||||||
|
|
|
@ -1,633 +0,0 @@
|
||||||
/*
|
|
||||||
*
|
|
||||||
* * 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.testsuite.user.profile;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_USER;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.junit.Assert;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.keycloak.component.ComponentModel;
|
|
||||||
import org.keycloak.component.ComponentValidationException;
|
|
||||||
import org.keycloak.models.KeycloakSession;
|
|
||||||
import org.keycloak.models.UserModel;
|
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
|
||||||
import org.keycloak.testsuite.user.profile.config.DeclarativeUserProfileProvider;
|
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
|
||||||
import org.keycloak.testsuite.user.profile.config.UPConfig;
|
|
||||||
import org.keycloak.testsuite.user.profile.config.UPConfigUtils;
|
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
|
||||||
import org.keycloak.userprofile.UserProfile;
|
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
|
||||||
import org.keycloak.userprofile.ValidationException;
|
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
import org.keycloak.validate.validators.LengthValidator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
|
||||||
*/
|
|
||||||
public class UserProfileConfigTest extends AbstractUserProfileTest {
|
|
||||||
|
|
||||||
protected static final String ATT_ADDRESS = "address";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
|
||||||
KeycloakModelUtils.createClient(testRealm, "client-a");
|
|
||||||
KeycloakModelUtils.createClient(testRealm, "client-b");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testConfigurationSetInvalid() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationSetInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testConfigurationSetInvalid(KeycloakSession session) {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
|
|
||||||
try {
|
|
||||||
provider.setConfiguration("{\"validateConfigAttribute\": true}");
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ComponentValidationException ve) {
|
|
||||||
// OK
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testConfigurationGetSet() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationGetSet);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testConfigurationGetSet(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
// generate big configuration to test slicing in the persistence/component config
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
for (int i = 0; i < 80; i++) {
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
attribute.setName(UserModel.USERNAME+i);
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
|
||||||
validatorConfig.put("min", 3);
|
|
||||||
attribute.addValidation("length", validatorConfig);
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
}
|
|
||||||
String newConfig = JsonSerialization.writeValueAsString(config);
|
|
||||||
|
|
||||||
provider.setConfiguration(newConfig);
|
|
||||||
// assert config is persisted in 2 pieces
|
|
||||||
Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
|
||||||
// assert config is returned correctly
|
|
||||||
Assert.assertEquals(newConfig, provider.getConfiguration());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testConfigurationGetSetDefault() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testConfigurationGetSetDefault);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testConfigurationGetSetDefault(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
|
|
||||||
provider.setConfiguration(null);
|
|
||||||
|
|
||||||
Assert.assertNull(provider.getComponentModel().get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
|
||||||
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
Assert.assertTrue(component.getConfig().isEmpty());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testDefaultConfigForUpdateProfile() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testDefaultConfigForUpdateProfile);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testDefaultConfigForUpdateProfile(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
|
|
||||||
// reset configuration to default
|
|
||||||
provider.setConfiguration(null);
|
|
||||||
|
|
||||||
// failed required validations
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, Collections.emptyMap());
|
|
||||||
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
// failed for blank values also
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.FIRST_NAME, "");
|
|
||||||
attributes.put(UserModel.LAST_NAME, " ");
|
|
||||||
attributes.put(UserModel.EMAIL, "");
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.EMAIL));
|
|
||||||
}
|
|
||||||
|
|
||||||
// all OK
|
|
||||||
attributes.put(UserModel.USERNAME, "jdoeusername");
|
|
||||||
attributes.put(UserModel.FIRST_NAME, "John");
|
|
||||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
|
||||||
attributes.put(UserModel.EMAIL, "jdoe@acme.org");
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testAdditionalValidationForUsername() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testAdditionalValidationForUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testAdditionalValidationForUsername(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(UserModel.USERNAME);
|
|
||||||
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
|
||||||
|
|
||||||
validatorConfig.put("min", 4);
|
|
||||||
|
|
||||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "us");
|
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
|
||||||
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
|
|
||||||
}
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
provider.setConfiguration(null);
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "us");
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
|
|
||||||
profile.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testFirstLastNameCanBeOptional() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testFirstLastNameCanBeOptional);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testFirstLastNameCanBeOptional(KeycloakSession session) throws IOException {
|
|
||||||
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
attribute.setName(UserModel.FIRST_NAME);
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
|
||||||
validatorConfig.put(LengthValidator.KEY_MAX, 4);
|
|
||||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
attribute = new UPAttribute();
|
|
||||||
attribute.setName(UserModel.LAST_NAME);
|
|
||||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
// not present attributes are OK
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
//empty attributes are OK
|
|
||||||
attributes.put(UserModel.FIRST_NAME, "");
|
|
||||||
attributes.put(UserModel.LAST_NAME, "");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
//filled attributes are OK
|
|
||||||
attributes.put(UserModel.FIRST_NAME, "John");
|
|
||||||
attributes.put(UserModel.LAST_NAME, "Doe");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// fails due to additional length validation so it is executed correctly
|
|
||||||
attributes.put(UserModel.FIRST_NAME, "JohnTooLong");
|
|
||||||
attributes.put(UserModel.LAST_NAME, "DoeTooLong");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
|
||||||
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCustomAttribute_Required() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute_Required);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testCustomAttribute_Required(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(ATT_ADDRESS);
|
|
||||||
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
|
||||||
|
|
||||||
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
|
||||||
|
|
||||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
|
||||||
|
|
||||||
// make it ALWAYS required
|
|
||||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
|
||||||
attribute.setRequired(requirements);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
|
|
||||||
// fails on required validation
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// fails on length validation
|
|
||||||
attributes.put(ATT_ADDRESS, "adr");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// all OK
|
|
||||||
attributes.put(ATT_ADDRESS, "adress ok");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testCustomAttribute_Optional() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testCustomAttribute_Optional);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testCustomAttribute_Optional(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(ATT_ADDRESS);
|
|
||||||
|
|
||||||
Map<String, Object> validatorConfig = new HashMap<>();
|
|
||||||
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
|
||||||
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
// null is OK as attribute is optional
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
//blank String have to be OK as it is what UI forms send for not filled in optional attributes
|
|
||||||
attributes.put(ATT_ADDRESS, "");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// fails on length validation
|
|
||||||
attributes.put(ATT_ADDRESS, "adr");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// all OK
|
|
||||||
attributes.put(ATT_ADDRESS, "adress ok");
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequiredByUserRole_USER() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByUserRole_USER);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testRequiredByUserRole_USER(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(ATT_ADDRESS);
|
|
||||||
|
|
||||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
|
||||||
|
|
||||||
List<String> roles = new ArrayList<>();
|
|
||||||
roles.add(ROLE_USER);
|
|
||||||
requirements.setRoles(roles);
|
|
||||||
|
|
||||||
attribute.setRequired(requirements);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
// fail on common contexts
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
|
||||||
try {
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
// no fail on User API
|
|
||||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
|
||||||
profile.validate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequiredByUserRole_ADMIN() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByUserRole_ADMIN);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testRequiredByUserRole_ADMIN(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(ATT_ADDRESS);
|
|
||||||
|
|
||||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
|
||||||
|
|
||||||
List<String> roles = new ArrayList<>();
|
|
||||||
roles.add(UPConfigUtils.ROLE_ADMIN);
|
|
||||||
requirements.setRoles(roles);
|
|
||||||
|
|
||||||
attribute.setRequired(requirements);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
// NO fail on common contexts
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// fail on User API
|
|
||||||
try {
|
|
||||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testRequiredByScope_clientDefaultScope() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileConfigTest::testRequiredByScope_clientDefaultScope);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void testRequiredByScope_clientDefaultScope(KeycloakSession session) throws IOException {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
|
||||||
ComponentModel component = provider.getComponentModel();
|
|
||||||
|
|
||||||
assertNotNull(component);
|
|
||||||
|
|
||||||
UPConfig config = new UPConfig();
|
|
||||||
UPAttribute attribute = new UPAttribute();
|
|
||||||
|
|
||||||
attribute.setName(ATT_ADDRESS);
|
|
||||||
|
|
||||||
UPAttributeRequired requirements = new UPAttributeRequired();
|
|
||||||
|
|
||||||
List<String> scopes = new ArrayList<>();
|
|
||||||
scopes.add("client-a");
|
|
||||||
requirements.setScopes(scopes);
|
|
||||||
|
|
||||||
attribute.setRequired(requirements);
|
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, "user");
|
|
||||||
|
|
||||||
// client with default scopes for which is attribute NOT configured as required
|
|
||||||
configureAuthenticationSession(session, "client-b", null);
|
|
||||||
|
|
||||||
// no fail on User API nor Account console as they do not have scopes
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// no fail on auth flow scopes when scope is not required
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// client with default scope for which is attribute configured as required
|
|
||||||
configureAuthenticationSession(session, "client-a", null);
|
|
||||||
|
|
||||||
// no fail on User API nor Account console as they do not have scopes
|
|
||||||
profile = provider.create(UserProfileContext.USER_API, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
|
||||||
profile.validate();
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
|
||||||
profile.validate();
|
|
||||||
|
|
||||||
// fail on auth flow scopes when scope is required
|
|
||||||
try {
|
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
|
||||||
profile.validate();
|
|
||||||
fail("Should fail validation");
|
|
||||||
} catch (ValidationException ve) {
|
|
||||||
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -26,6 +26,9 @@ 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.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||||
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -38,19 +41,26 @@ import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.component.ComponentModel;
|
||||||
|
import org.keycloak.component.ComponentValidationException;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
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.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
|
||||||
|
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttribute;
|
import org.keycloak.userprofile.UserProfileSpi;
|
||||||
import org.keycloak.testsuite.user.profile.config.UPAttributeRequired;
|
import org.keycloak.userprofile.config.DeclarativeUserProfileProvider;
|
||||||
import org.keycloak.testsuite.user.profile.config.UPConfig;
|
import org.keycloak.userprofile.config.UPAttribute;
|
||||||
|
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||||
|
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||||
|
import org.keycloak.userprofile.config.UPConfig;
|
||||||
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
import org.keycloak.testsuite.util.ClientScopeBuilder;
|
||||||
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||||
import org.keycloak.userprofile.Attributes;
|
import org.keycloak.userprofile.Attributes;
|
||||||
|
@ -58,13 +68,19 @@ import org.keycloak.userprofile.UserProfile;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
import org.keycloak.userprofile.UserProfileProvider;
|
import org.keycloak.userprofile.UserProfileProvider;
|
||||||
import org.keycloak.userprofile.ValidationException;
|
import org.keycloak.userprofile.ValidationException;
|
||||||
|
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
import org.keycloak.validate.ValidationError;
|
import org.keycloak.validate.ValidationError;
|
||||||
import org.keycloak.validate.validators.EmailValidator;
|
import org.keycloak.validate.validators.EmailValidator;
|
||||||
|
import org.keycloak.validate.validators.LengthValidator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
*/
|
*/
|
||||||
|
@EnableFeature(Profile.Feature.DECLARATIVE_USER_PROFILE)
|
||||||
|
@SetDefaultProvider(spi = UserProfileSpi.ID, providerId = DeclarativeUserProfileProvider.ID,
|
||||||
|
beforeEnableFeature = false,
|
||||||
|
onlyUpdateDefault = true)
|
||||||
public class UserProfileTest extends AbstractUserProfileTest {
|
public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -72,21 +88,12 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build()));
|
testRealm.setClientScopes(Collections.singletonList(ClientScopeBuilder.create().name("customer").protocol("openid-connect").build()));
|
||||||
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
|
ClientRepresentation client = KeycloakModelUtils.createClient(testRealm, "client-a");
|
||||||
client.setDefaultClientScopes(Collections.singletonList("customer"));
|
client.setDefaultClientScopes(Collections.singletonList("customer"));
|
||||||
}
|
KeycloakModelUtils.createClient(testRealm, "client-b");
|
||||||
|
|
||||||
@After
|
|
||||||
public void onAfter() {
|
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::resetConfiguration);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void resetConfiguration(KeycloakSession session) {
|
|
||||||
configureSessionRealm(session);
|
|
||||||
getDynamicUserProfileProvider(session).setConfiguration(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIdempotentProfile() {
|
public void testIdempotentProfile() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testIdempotentProfile);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testIdempotentProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testIdempotentProfile(KeycloakSession session) {
|
private static void testIdempotentProfile(KeycloakSession session) {
|
||||||
|
@ -103,7 +110,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCustomAttributeInAnyContext() {
|
public void testCustomAttributeInAnyContext() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeInAnyContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testCustomAttributeInAnyContext(KeycloakSession session) {
|
private static void testCustomAttributeInAnyContext(KeycloakSession session) {
|
||||||
|
@ -113,7 +120,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
@ -137,7 +144,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testResolveProfile() {
|
public void testResolveProfile() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testResolveProfile);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResolveProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testResolveProfile(KeycloakSession session) {
|
private static void testResolveProfile(KeycloakSession session) {
|
||||||
|
@ -149,7 +156,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}}]}");
|
provider.setConfiguration("{\"attributes\": [{\"name\": \"business.address\", \"required\": {\"scopes\": [\"customer\"]}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
@ -173,13 +180,14 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidation() {
|
public void testValidation() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::failValidationWhenEmptyAttributes);
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testAttributeValidation);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testAttributeValidation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void failValidationWhenEmptyAttributes(KeycloakSession session) {
|
private static void failValidationWhenEmptyAttributes(KeycloakSession session) {
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
UserProfileProvider provider = session.getProvider(UserProfileProvider.class);
|
||||||
|
provider.setConfiguration(null);
|
||||||
UserProfile profile;
|
UserProfile profile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -207,6 +215,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
try {
|
try {
|
||||||
realm.setRegistrationEmailAsUsername(true);
|
realm.setRegistrationEmailAsUsername(true);
|
||||||
attributes.clear();
|
attributes.clear();
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
|
attributes.put(UserModel.EMAIL, "profile-user@keycloak.org");
|
||||||
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
profile.validate();
|
profile.validate();
|
||||||
|
@ -219,6 +229,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
attributes.clear();
|
attributes.clear();
|
||||||
attributes.put(UserModel.USERNAME, "profile-user");
|
attributes.put(UserModel.USERNAME, "profile-user");
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
provider.create(UserProfileContext.UPDATE_PROFILE, attributes).validate();
|
provider.create(UserProfileContext.UPDATE_PROFILE, attributes).validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,11 +264,11 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testValidateComplianceWithUserProfile() {
|
public void testValidateComplianceWithUserProfile() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testValidateComplianceWithUserProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testValidateComplianceWithUserProfile(KeycloakSession session) throws IOException {
|
private static void testValidateComplianceWithUserProfile(KeycloakSession session) throws IOException {
|
||||||
RealmModel realm = configureSessionRealm(session);
|
RealmModel realm = session.getContext().getRealm();
|
||||||
UserModel user = session.users().addUser(realm, "profiled-user");
|
UserModel user = session.users().addUser(realm, "profiled-user");
|
||||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
|
@ -269,6 +281,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
attribute.setRequired(requirements);
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
config.addAttribute(attribute);
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
@ -292,15 +308,15 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testGetProfileAttributes() {
|
public void testGetProfileAttributes() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testGetProfileAttributes);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testGetProfileAttributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testGetProfileAttributes(KeycloakSession session) {
|
private static void testGetProfileAttributes(KeycloakSession session) {
|
||||||
RealmModel realm = configureSessionRealm(session);
|
RealmModel realm = session.getContext().getRealm();
|
||||||
UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
UserModel user = session.users().addUser(realm, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||||
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
UserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}}]}");
|
provider.setConfiguration("{\"attributes\": [{\"name\": \"address\", \"required\": {}, \"permissions\": {\"edit\": [\"user\"]}}]}");
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, user);
|
||||||
Attributes attributes = profile.getAttributes();
|
Attributes attributes = profile.getAttributes();
|
||||||
|
@ -334,7 +350,7 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testCreateAndUpdateUser() {
|
public void testCreateAndUpdateUser() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCreateAndUpdateUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testCreateAndUpdateUser(KeycloakSession session) {
|
private static void testCreateAndUpdateUser(KeycloakSession session) {
|
||||||
|
@ -343,6 +359,8 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
String userName = org.keycloak.models.utils.KeycloakModelUtils.generateId();
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, userName);
|
attributes.put(UserModel.USERNAME, userName);
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
attributes.put("address", "fixed-address");
|
attributes.put("address", "fixed-address");
|
||||||
|
|
||||||
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
UserProfile profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||||
|
@ -377,12 +395,10 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReadonlyUpdates() {
|
public void testReadonlyUpdates() {
|
||||||
getTestingClient().server().run((RunOnServer) UserProfileTest::testReadonlyUpdates);
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testReadonlyUpdates);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void testReadonlyUpdates(KeycloakSession session) {
|
private static void testReadonlyUpdates(KeycloakSession session) {
|
||||||
configureSessionRealm(session);
|
|
||||||
|
|
||||||
Map<String, Object> attributes = new HashMap<>();
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
attributes.put(UserModel.USERNAME, org.keycloak.models.utils.KeycloakModelUtils.generateId());
|
||||||
|
@ -415,10 +431,684 @@ public class UserProfileTest extends AbstractUserProfileTest {
|
||||||
|
|
||||||
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
profile = provider.create(UserProfileContext.ACCOUNT, attributes, user);
|
||||||
|
|
||||||
profile.update();
|
try {
|
||||||
|
profile.update();
|
||||||
|
fail("Should fail due to read only attribute");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError("department"));
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals("sales", user.getFirstAttribute("department"));
|
assertEquals("sales", user.getFirstAttribute("department"));
|
||||||
|
|
||||||
assertTrue(profile.getAttributes().isReadOnly("department"));
|
assertTrue(profile.getAttributes().isReadOnly("department"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static final String ATT_ADDRESS = "address";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidConfiguration() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testInvalidConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testInvalidConfiguration(KeycloakSession session) {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
|
try {
|
||||||
|
provider.setConfiguration("{\"validateConfigAttribute\": true}");
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ComponentValidationException ve) {
|
||||||
|
// OK
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConfigurationChunks() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testConfigurationChunks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testConfigurationChunks(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
// generate big configuration to test slicing in the persistence/component config
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
for (int i = 0; i < 80; i++) {
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
attribute.setName(UserModel.USERNAME+i);
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
validatorConfig.put("min", 3);
|
||||||
|
attribute.addValidation("length", validatorConfig);
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
}
|
||||||
|
String newConfig = JsonSerialization.writeValueAsString(config);
|
||||||
|
|
||||||
|
provider.setConfiguration(newConfig);
|
||||||
|
|
||||||
|
component = provider.getComponentModel();
|
||||||
|
|
||||||
|
// assert config is persisted in 2 pieces
|
||||||
|
Assert.assertEquals("2", component.get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||||
|
// assert config is returned correctly
|
||||||
|
Assert.assertEquals(newConfig, provider.getConfiguration());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResetConfiguration() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testResetConfiguration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testResetConfiguration(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
|
provider.setConfiguration(null);
|
||||||
|
|
||||||
|
Assert.assertNull(provider.getComponentModel().get(DeclarativeUserProfileProvider.UP_PIECES_COUNT_COMPONENT_CONFIG_KEY));
|
||||||
|
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
Assert.assertTrue(component.getConfig().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultConfig() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testDefaultConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testDefaultConfig(KeycloakSession session) {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
|
||||||
|
// reset configuration to default
|
||||||
|
provider.setConfiguration(null);
|
||||||
|
|
||||||
|
// failed required validations
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, Collections.emptyMap());
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
// failed for blank values also
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "");
|
||||||
|
attributes.put(UserModel.LAST_NAME, " ");
|
||||||
|
attributes.put(UserModel.EMAIL, "");
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.EMAIL));
|
||||||
|
}
|
||||||
|
|
||||||
|
// all OK
|
||||||
|
attributes.put(UserModel.USERNAME, "jdoeusername");
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "John");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
attributes.put(UserModel.EMAIL, "jdoe@acme.org");
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomValidationForUsername() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomValidationForUsername);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testCustomValidationForUsername(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(UserModel.USERNAME);
|
||||||
|
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
|
||||||
|
validatorConfig.put("min", 4);
|
||||||
|
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "us");
|
||||||
|
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.USERNAME));
|
||||||
|
assertTrue(ve.hasError(LengthValidator.MESSAGE_INVALID_LENGTH));
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
provider.setConfiguration(null);
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "us");
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testOptionalAttributes() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testOptionalAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testOptionalAttributes(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
attribute.setName(UserModel.FIRST_NAME);
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
validatorConfig.put(LengthValidator.KEY_MAX, 4);
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
attribute = new UPAttribute();
|
||||||
|
attribute.setName(UserModel.LAST_NAME);
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// not present attributes are OK
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
//empty attributes are OK
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
//filled attributes are OK
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "John");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// fails due to additional length validation so it is executed correctly
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "JohnTooLong");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "DoeTooLong");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.FIRST_NAME));
|
||||||
|
assertTrue(ve.isAttributeOnError(UserModel.LAST_NAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomAttributeRequired() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeRequired);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testCustomAttributeRequired(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
|
||||||
|
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
||||||
|
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
|
// make it ALWAYS required
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
|
||||||
|
// fails on required validation
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// fails on length validation
|
||||||
|
attributes.put(ATT_ADDRESS, "adr");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// all OK
|
||||||
|
attributes.put(ATT_ADDRESS, "adress ok");
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCustomAttributeOptional() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testCustomAttributeOptional);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testCustomAttributeOptional(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
Map<String, Object> validatorConfig = new HashMap<>();
|
||||||
|
validatorConfig.put(LengthValidator.KEY_MIN, 4);
|
||||||
|
attribute.addValidation(LengthValidator.ID, validatorConfig);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// null is OK as attribute is optional
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
//blank String have to be OK as it is what UI forms send for not filled in optional attributes
|
||||||
|
attributes.put(ATT_ADDRESS, "");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// fails on length validation
|
||||||
|
attributes.put(ATT_ADDRESS, "adr");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// all OK
|
||||||
|
attributes.put(ATT_ADDRESS, "adress ok");
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredIfUser() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testRequiredIfUser(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
|
||||||
|
List<String> roles = new ArrayList<>();
|
||||||
|
roles.add(ROLE_USER);
|
||||||
|
requirements.setRoles(roles);
|
||||||
|
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList(ROLE_USER));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// fail on common contexts
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "Joe");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "Doe");
|
||||||
|
|
||||||
|
// no fail on User API
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredIfAdmin() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredIfAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testRequiredIfAdmin(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
|
||||||
|
requirements.setRoles(Collections.singletonList(ROLE_ADMIN));
|
||||||
|
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// NO fail on common contexts
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// fail on User API
|
||||||
|
try {
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoValidationsIfUserReadOnly() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfUserReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testNoValidationsIfUserReadOnly(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList(UPConfigUtils.ROLE_ADMIN));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
attributes.put(UserModel.FIRST_NAME, "user");
|
||||||
|
attributes.put(UserModel.LAST_NAME, "user");
|
||||||
|
|
||||||
|
// NO fail on USER contexts
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// Fails on ADMIN context - User REST API
|
||||||
|
try {
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoValidationsIfAdminReadOnly() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testNoValidationsIfAdminReadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testNoValidationsIfAdminReadOnly(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
List<String> roles = new ArrayList<>();
|
||||||
|
roles.add(UPConfigUtils.ROLE_USER);
|
||||||
|
permissions.setEdit(roles);
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// Fails on USER context
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
try {
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NO fail on ADMIN context - User REST API
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRequiredByClientScope() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) UserProfileTest::testRequiredByClientScope);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testRequiredByClientScope(KeycloakSession session) throws IOException {
|
||||||
|
DeclarativeUserProfileProvider provider = getDynamicUserProfileProvider(session);
|
||||||
|
ComponentModel component = provider.getComponentModel();
|
||||||
|
|
||||||
|
assertNotNull(component);
|
||||||
|
|
||||||
|
UPConfig config = new UPConfig();
|
||||||
|
UPAttribute attribute = new UPAttribute();
|
||||||
|
|
||||||
|
attribute.setName(ATT_ADDRESS);
|
||||||
|
|
||||||
|
UPAttributeRequired requirements = new UPAttributeRequired();
|
||||||
|
|
||||||
|
List<String> scopes = new ArrayList<>();
|
||||||
|
scopes.add("client-a");
|
||||||
|
requirements.setScopes(scopes);
|
||||||
|
|
||||||
|
attribute.setRequired(requirements);
|
||||||
|
|
||||||
|
UPAttributePermissions permissions = new UPAttributePermissions();
|
||||||
|
permissions.setEdit(Collections.singletonList("user"));
|
||||||
|
attribute.setPermissions(permissions);
|
||||||
|
|
||||||
|
config.addAttribute(attribute);
|
||||||
|
|
||||||
|
provider.setConfiguration(JsonSerialization.writeValueAsString(config));
|
||||||
|
|
||||||
|
Map<String, Object> attributes = new HashMap<>();
|
||||||
|
|
||||||
|
attributes.put(UserModel.USERNAME, "user");
|
||||||
|
|
||||||
|
// client with default scopes for which is attribute NOT configured as required
|
||||||
|
configureAuthenticationSession(session, "client-b", null);
|
||||||
|
|
||||||
|
// no fail on User API nor Account console as they do not have scopes
|
||||||
|
UserProfile profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// no fail on auth flow scopes when scope is not required
|
||||||
|
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.REGISTRATION_USER_CREATION, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// client with default scope for which is attribute configured as required
|
||||||
|
configureAuthenticationSession(session, "client-a", null);
|
||||||
|
|
||||||
|
// no fail on User API nor Account console as they do not have scopes
|
||||||
|
profile = provider.create(UserProfileContext.USER_API, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT, attributes);
|
||||||
|
profile.validate();
|
||||||
|
profile = provider.create(UserProfileContext.ACCOUNT_OLD, attributes);
|
||||||
|
profile.validate();
|
||||||
|
|
||||||
|
// fail on auth flow scopes when scope is required
|
||||||
|
try {
|
||||||
|
profile = provider.create(UserProfileContext.UPDATE_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
profile = provider.create(UserProfileContext.REGISTRATION_PROFILE, attributes);
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
profile = provider.create(UserProfileContext.IDP_REVIEW, attributes);
|
||||||
|
profile.validate();
|
||||||
|
fail("Should fail validation");
|
||||||
|
} catch (ValidationException ve) {
|
||||||
|
assertTrue(ve.isAttributeOnError(ATT_ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.testsuite.user.profile.config;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.readConfig;
|
import static org.keycloak.userprofile.config.UPConfigUtils.readConfig;
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.validate;
|
import static org.keycloak.userprofile.config.UPConfigUtils.validate;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -35,6 +35,11 @@ import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
import org.keycloak.testsuite.runonserver.RunOnServer;
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import org.keycloak.userprofile.config.UPAttribute;
|
||||||
|
import org.keycloak.userprofile.config.UPAttributePermissions;
|
||||||
|
import org.keycloak.userprofile.config.UPAttributeRequired;
|
||||||
|
import org.keycloak.userprofile.config.UPConfig;
|
||||||
|
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link UPConfigParser} functionality
|
* Unit test for {@link UPConfigParser} functionality
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.testsuite.user.profile.config;
|
package org.keycloak.testsuite.user.profile.config;
|
||||||
|
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_ADMIN;
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_ADMIN;
|
||||||
import static org.keycloak.testsuite.user.profile.config.UPConfigUtils.ROLE_USER;
|
import static org.keycloak.userprofile.config.UPConfigUtils.ROLE_USER;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -25,6 +25,7 @@ import java.util.List;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.keycloak.userprofile.UserProfileContext;
|
import org.keycloak.userprofile.UserProfileContext;
|
||||||
|
import org.keycloak.userprofile.config.UPConfigUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit test for {@link UPConfigUtils}
|
* Unit test for {@link UPConfigUtils}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
*
|
||||||
|
* * 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.testsuite.validation;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.runonserver.RunOnServer;
|
||||||
|
import org.keycloak.validate.ValidationContext;
|
||||||
|
import org.keycloak.validate.Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||||
|
*/
|
||||||
|
public class ValidatorTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
testRealm.user("alice");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDateValidator() {
|
||||||
|
getTestingClient().server(TEST_REALM_NAME).run((RunOnServer) ValidatorTest::testDateValidator);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void testDateValidator(KeycloakSession session) {
|
||||||
|
assertTrue(Validators.dateValidator().validate(null, new ValidationContext(session)).isValid());
|
||||||
|
assertTrue(Validators.dateValidator().validate("", new ValidationContext(session)).isValid());
|
||||||
|
|
||||||
|
// defaults to Locale.ENGLISH as per default locale selector
|
||||||
|
assertFalse(Validators.dateValidator().validate("13/12/2021", new ValidationContext(session)).isValid());
|
||||||
|
assertFalse(Validators.dateValidator().validate("13/12/21", new ValidationContext(session)).isValid());
|
||||||
|
assertTrue(Validators.dateValidator().validate("12/13/2021", new ValidationContext(session)).isValid());
|
||||||
|
RealmModel realm = session.getContext().getRealm();
|
||||||
|
|
||||||
|
realm.setInternationalizationEnabled(true);
|
||||||
|
realm.setDefaultLocale(Locale.FRANCE.getLanguage());
|
||||||
|
|
||||||
|
assertTrue(Validators.dateValidator().validate("13/12/21", new ValidationContext(session)).isValid());
|
||||||
|
assertTrue(Validators.dateValidator().validate("13/12/2021", new ValidationContext(session)).isValid());
|
||||||
|
assertFalse(Validators.dateValidator().validate("12/13/2021", new ValidationContext(session)).isValid());
|
||||||
|
|
||||||
|
UserModel alice = session.users().getUserByUsername(realm, "alice");
|
||||||
|
|
||||||
|
alice.setAttribute(UserModel.LOCALE, Collections.singletonList(Locale.ENGLISH.getLanguage()));
|
||||||
|
|
||||||
|
ValidationContext context = new ValidationContext(session);
|
||||||
|
|
||||||
|
context.getAttributes().put(UserModel.class.getName(), alice);
|
||||||
|
|
||||||
|
assertFalse(Validators.dateValidator().validate("13/12/2021", context).isValid());
|
||||||
|
}
|
||||||
|
}
|
|
@ -219,9 +219,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"userProfile": {
|
"userProfile": {
|
||||||
|
"provider": "${keycloak.userProfile.provider:}",
|
||||||
"legacy-user-profile": {
|
"legacy-user-profile": {
|
||||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||||
|
},
|
||||||
|
"declarative-user-profile": {
|
||||||
|
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||||
|
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -139,9 +139,14 @@
|
||||||
},
|
},
|
||||||
|
|
||||||
"userProfile": {
|
"userProfile": {
|
||||||
|
"provider": "${keycloak.userProfile.provider:}",
|
||||||
"legacy-user-profile": {
|
"legacy-user-profile": {
|
||||||
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||||
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||||
|
},
|
||||||
|
"declarative-user-profile": {
|
||||||
|
"read-only-attributes": [ "deniedFoo", "deniedBar*", "deniedSome/thing", "deniedsome*thing" ],
|
||||||
|
"admin-read-only-attributes": [ "deniedSomeAdmin" ]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -374,3 +374,16 @@ openshift.scope.user_info=User information
|
||||||
openshift.scope.user_check-access=User access information
|
openshift.scope.user_check-access=User access information
|
||||||
openshift.scope.user_full=Full Access
|
openshift.scope.user_full=Full Access
|
||||||
openshift.scope.list-projects=List projects
|
openshift.scope.list-projects=List projects
|
||||||
|
|
||||||
|
error-invalid-value=Invalid value.
|
||||||
|
error-invalid-blank=Please specify value.
|
||||||
|
error-empty=Please specify value.
|
||||||
|
error-invalid-length=Attribute {0} must have a length between {1} and {2}.
|
||||||
|
error-invalid-email=Invalid email address.
|
||||||
|
error-invalid-number=Invalid number.
|
||||||
|
error-number-out-of-range=Attribute {0} must be a number between {1} and {2}.
|
||||||
|
error-pattern-no-match=Invalid value.
|
||||||
|
error-invalid-uri=Invalid URL.
|
||||||
|
error-invalid-uri-scheme=Invalid URL scheme.
|
||||||
|
error-invalid-uri-fragment=Invalid URL fragment.
|
||||||
|
error-user-attribute-required=Please specify attribute {0}.
|
|
@ -221,6 +221,7 @@ realm-tab-cache=Cache
|
||||||
realm-tab-tokens=Tokens
|
realm-tab-tokens=Tokens
|
||||||
realm-tab-client-registration=Client Registration
|
realm-tab-client-registration=Client Registration
|
||||||
realm-tab-security-defenses=Security Defenses
|
realm-tab-security-defenses=Security Defenses
|
||||||
|
realm-tab-user-profile=User Profile
|
||||||
realm-tab-general=General
|
realm-tab-general=General
|
||||||
add-realm=Add realm
|
add-realm=Add realm
|
||||||
|
|
||||||
|
@ -1882,3 +1883,23 @@ dialogs.delete.message=Are you sure you want to permanently delete the {{type}}
|
||||||
dialogs.delete.confirm=Delete
|
dialogs.delete.confirm=Delete
|
||||||
dialogs.cancel=Cancel
|
dialogs.cancel=Cancel
|
||||||
dialogs.ok=Ok
|
dialogs.ok=Ok
|
||||||
|
|
||||||
|
user.profile.attribute=Attribute
|
||||||
|
user.profile.attribute.name=Name
|
||||||
|
user.profile.attribute.name.tooltip=The name of the attribute.
|
||||||
|
user.profile.attribute.required=Required
|
||||||
|
user.profile.attribute.required.tooltip=Set the attribute as required. If enabled, the attribute must be set by users and administrators. Otherwise, the attribute is optional.
|
||||||
|
user.profile.attribute.permission=Permission
|
||||||
|
user.profile.attribute.canUserView=Can user view?
|
||||||
|
user.profile.attribute.canUserView.tooltip=If enabled, users can view the attribute. Otherwise, users don't have access to the attribute.
|
||||||
|
user.profile.attribute.canUserEdit=Can user edit?
|
||||||
|
user.profile.attribute.canUserEdit.tooltip=If enabled, users can view and edit the attribute. Otherwise, users don't have access to write to the attribute.
|
||||||
|
user.profile.attribute.canAdminView=Can admin view?
|
||||||
|
user.profile.attribute.canAdminView.tooltip=If enabled, administrators can view the attribute. Otherwise, administrators don't have access to the attribute.
|
||||||
|
user.profile.attribute.canAdminEdit=Can admin edit?
|
||||||
|
user.profile.attribute.canAdminEdit.tooltip=If enabled, administrators can view and edit the attribute. Otherwise, administrators don't have access to write to the attribute.
|
||||||
|
user.profile.attribute.validation=Validation
|
||||||
|
user.profile.attribute.validation.add.validator=Add Validator
|
||||||
|
user.profile.attribute.validation.add.validator.tooltip=Select a validator to enforce specific constraints to the attribute value.
|
||||||
|
user.profile.attribute.validation.no.validators=No validators.
|
||||||
|
user.profile.attribute.annotation=Annotation
|
|
@ -39,3 +39,19 @@ pairwiseClientRedirectURIsMultipleHosts=Without a configured Sector Identifier U
|
||||||
pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI.
|
pairwiseMalformedSectorIdentifierURI=Malformed Sector Identifier URI.
|
||||||
pairwiseFailedToGetRedirectURIs=Failed to get redirect URIs from the Sector Identifier URI.
|
pairwiseFailedToGetRedirectURIs=Failed to get redirect URIs from the Sector Identifier URI.
|
||||||
pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.
|
pairwiseRedirectURIsMismatch=Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.
|
||||||
|
|
||||||
|
error-invalid-value=Invalid value.
|
||||||
|
error-invalid-blank=Please specify value.
|
||||||
|
error-empty=Please specify value.
|
||||||
|
error-invalid-length=Attribute {0} must have a length between {1} and {2}.
|
||||||
|
error-invalid-email=Invalid email address.
|
||||||
|
error-invalid-number=Invalid number.
|
||||||
|
error-number-out-of-range=Attribute {0} must be a number between {1} and {2}.
|
||||||
|
error-pattern-no-match=Invalid value.
|
||||||
|
error-invalid-uri=Invalid URL.
|
||||||
|
error-invalid-uri-scheme=Invalid URL scheme.
|
||||||
|
error-invalid-uri-fragment=Invalid URL fragment.
|
||||||
|
error-user-attribute-required=Please specify attribute {0}.
|
||||||
|
error-invalid-date=Invalid date.
|
||||||
|
|
||||||
|
error-user-attribute-read-only=Attribute {0} is read only.
|
|
@ -260,6 +260,18 @@ module.config([ '$routeProvider', function($routeProvider) {
|
||||||
},
|
},
|
||||||
controller : 'RealmTokenDetailCtrl'
|
controller : 'RealmTokenDetailCtrl'
|
||||||
})
|
})
|
||||||
|
.when('/realms/:realm/user-profile', {
|
||||||
|
templateUrl : resourceUrl + '/partials/realm-user-profile.html',
|
||||||
|
resolve : {
|
||||||
|
serverInfo : function(ServerInfoLoader) {
|
||||||
|
return ServerInfoLoader();
|
||||||
|
},
|
||||||
|
realm : function(RealmLoader) {
|
||||||
|
return RealmLoader();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
controller : 'RealmUserProfileCtrl'
|
||||||
|
})
|
||||||
.when('/realms/:realm/client-registration/client-initial-access', {
|
.when('/realms/:realm/client-registration/client-initial-access', {
|
||||||
templateUrl : resourceUrl + '/partials/client-initial-access.html',
|
templateUrl : resourceUrl + '/partials/client-initial-access.html',
|
||||||
resolve : {
|
resolve : {
|
||||||
|
@ -2433,6 +2445,14 @@ module.factory('errorInterceptor', function($q, $window, $rootScope, $location,
|
||||||
} else if (response.status) {
|
} else if (response.status) {
|
||||||
if (response.data && response.data.errorMessage) {
|
if (response.data && response.data.errorMessage) {
|
||||||
Notifications.error(response.data.errorMessage);
|
Notifications.error(response.data.errorMessage);
|
||||||
|
} else if (response.data && response.data.errors) {
|
||||||
|
var messages = "Multiple errors found: ";
|
||||||
|
|
||||||
|
for (var i = 0; i < response.data.errors.length; i++) {
|
||||||
|
messages+=response.data.errors[i].errorMessage + " ";
|
||||||
|
}
|
||||||
|
|
||||||
|
Notifications.error(messages);
|
||||||
} else if (response.data && response.data.error_description) {
|
} else if (response.data && response.data.error_description) {
|
||||||
Notifications.error(response.data.error_description);
|
Notifications.error(response.data.error_description);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1401,6 +1401,232 @@ module.controller('RealmTokenDetailCtrl', function($scope, Realm, realm, $http,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.controller('RealmUserProfileCtrl', function($scope, Realm, realm, $http, $location, $route, UserProfile, Dialog, Notifications, serverInfo) {
|
||||||
|
$scope.realm = realm;
|
||||||
|
$scope.validatorProviders = serverInfo.componentTypes['org.keycloak.validate.Validator'];
|
||||||
|
|
||||||
|
$scope.isShowAttributes = true;
|
||||||
|
|
||||||
|
UserProfile.get({realm: realm.realm}, function(config) {
|
||||||
|
$scope.config = config;
|
||||||
|
$scope.rawConfig = angular.toJson(config, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.isShowAttributes = true;
|
||||||
|
|
||||||
|
$scope.showAttributes = function() {
|
||||||
|
$route.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.showJsonEditor = function() {
|
||||||
|
$scope.isShowAttributes = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.canViewPermission = {
|
||||||
|
minimumInputLength: 0,
|
||||||
|
delay: 500,
|
||||||
|
allowClear: true,
|
||||||
|
query: function (query) {
|
||||||
|
query.callback({results: ['user', 'admin']});
|
||||||
|
},
|
||||||
|
formatResult: function(object, container, query) {
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
formatSelection: function(object, container, query) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.canEditPermission = {
|
||||||
|
minimumInputLength: 0,
|
||||||
|
delay: 500,
|
||||||
|
allowClear: true,
|
||||||
|
query: function (query) {
|
||||||
|
query.callback({results: ['user', 'admin']});
|
||||||
|
},
|
||||||
|
formatResult: function(object, container, query) {
|
||||||
|
return object;
|
||||||
|
},
|
||||||
|
formatSelection: function(object, container, query) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.attributeSelected = false;
|
||||||
|
|
||||||
|
$scope.showListing = function() {
|
||||||
|
return !$scope.attributeSelected && $scope.currentAttribute == null && $scope.isShowAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.create = function() {
|
||||||
|
$scope.isCreate = true;
|
||||||
|
$scope.currentAttribute = {
|
||||||
|
permissions: {
|
||||||
|
view: [],
|
||||||
|
edit: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeAttribute = function(attribute) {
|
||||||
|
Dialog.confirmDelete(attribute.name, 'attribute', function() {
|
||||||
|
let newAttributes = [];
|
||||||
|
|
||||||
|
for (var v of $scope.config.attributes) {
|
||||||
|
if (v != attribute) {
|
||||||
|
newAttributes.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.config.attributes = newAttributes;
|
||||||
|
$scope.save();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.addAnnotation = function() {
|
||||||
|
if (!$scope.currentAttribute.annotations) {
|
||||||
|
$scope.currentAttribute.annotations = {};
|
||||||
|
}
|
||||||
|
$scope.currentAttribute.annotations[$scope.newAnnotation.key] = $scope.newAnnotation.value;
|
||||||
|
delete $scope.newAnnotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.removeAnnotation = function(key) {
|
||||||
|
delete $scope.currentAttribute.annotations[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.edit = function(attribute) {
|
||||||
|
if (attribute.permissions == null) {
|
||||||
|
attribute.permissions = {
|
||||||
|
view: [],
|
||||||
|
edit: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.isRequired = attribute.required != null;
|
||||||
|
$scope.canUserView = attribute.permissions.view.includes('user');
|
||||||
|
$scope.canAdminView = attribute.permissions.view.includes('admin');
|
||||||
|
$scope.canUserEdit = attribute.permissions.edit.includes('user');
|
||||||
|
$scope.canAdminEdit = attribute.permissions.edit.includes('admin');
|
||||||
|
$scope.currentAttribute = attribute;
|
||||||
|
$scope.attributeSelected = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('isRequired', function() {
|
||||||
|
if ($scope.isRequired) {
|
||||||
|
$scope.currentAttribute.required = {};
|
||||||
|
} else {
|
||||||
|
delete $scope.currentAttribute.required;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
handlePermission = function(permission, role, allowed) {
|
||||||
|
let attribute = $scope.currentAttribute;
|
||||||
|
let roles = [];
|
||||||
|
|
||||||
|
for (let r of attribute.permissions[permission]) {
|
||||||
|
if (r != role) {
|
||||||
|
roles.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
roles.push(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.permissions[permission] = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.$watch('canUserView', function() {
|
||||||
|
handlePermission('view', 'user', $scope.canUserView);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('canAdminView', function() {
|
||||||
|
handlePermission('view', 'admin', $scope.canAdminView);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('canUserEdit', function() {
|
||||||
|
handlePermission('edit', 'user', $scope.canUserEdit);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('canAdminEdit', function() {
|
||||||
|
handlePermission('edit', 'admin', $scope.canAdminEdit);
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
$scope.addValidator = function(validator) {
|
||||||
|
if ($scope.currentAttribute.validations == null) {
|
||||||
|
$scope.currentAttribute.validations = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
for (let key in validator.config) {
|
||||||
|
let values = validator.config[key];
|
||||||
|
|
||||||
|
for (let k in values) {
|
||||||
|
config[key] = values[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.currentAttribute.validations[validator.id] = config;
|
||||||
|
|
||||||
|
delete $scope.newValidator;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.selectValidator = function(validator) {
|
||||||
|
validator.config = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.cancelAddValidator = function() {
|
||||||
|
delete $scope.newValidator;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.removeValidator = function(id) {
|
||||||
|
let newValidators = {};
|
||||||
|
|
||||||
|
for (let v in $scope.currentAttribute.validations) {
|
||||||
|
if (v != id) {
|
||||||
|
newValidators[v] = $scope.currentAttribute.validations[v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValidators.length == 0) {
|
||||||
|
delete $scope.currentAttribute.validations;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.currentAttribute.validations = newValidators;
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.save = function() {
|
||||||
|
if (!$scope.isShowAttributes) {
|
||||||
|
$scope.config = JSON.parse($scope.rawConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($scope.isCreate && $scope.currentAttribute) {
|
||||||
|
$scope.config['attributes'].push($scope.currentAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
UserProfile.update({realm: realm.realm},
|
||||||
|
$scope.config, function () {
|
||||||
|
$scope.attributeSelected = false;
|
||||||
|
delete $scope.currentAttribute;
|
||||||
|
delete $scope.isCreate;
|
||||||
|
delete $scope.isRequired;
|
||||||
|
delete $scope.canUserView;
|
||||||
|
delete $scope.canAdminView;
|
||||||
|
delete $scope.canUserEdit;
|
||||||
|
delete $scope.canAdminEdit;
|
||||||
|
$route.reload();
|
||||||
|
Notifications.success("The attribute has been added.");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.reset = function() {
|
||||||
|
$route.reload();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
module.controller('ViewKeyCtrl', function($scope, key) {
|
module.controller('ViewKeyCtrl', function($scope, key) {
|
||||||
$scope.key = key;
|
$scope.key = key;
|
||||||
});
|
});
|
||||||
|
|
|
@ -2112,6 +2112,16 @@ module.factory('UserGroupMapping', function($resource) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.factory('UserProfile', function($resource) {
|
||||||
|
return $resource(authUrl + '/admin/realms/:realm/users/profile', {
|
||||||
|
realm : '@realm'
|
||||||
|
}, {
|
||||||
|
update : {
|
||||||
|
method : 'PUT'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.factory('DefaultGroups', function($resource) {
|
module.factory('DefaultGroups', function($resource) {
|
||||||
return $resource(authUrl + '/admin/realms/:realm/default-groups/:groupId', {
|
return $resource(authUrl + '/admin/realms/:realm/default-groups/:groupId', {
|
||||||
realm : '@realm',
|
realm : '@realm',
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||||
|
<kc-tabs-realm></kc-tabs-realm>
|
||||||
|
|
||||||
|
<ul class="nav nav-tabs nav-tabs-pf">
|
||||||
|
<li ng-class="{active: isShowAttributes}"><a href="" data-ng-click="showAttributes()">{{:: 'attributes' | translate}}</a></li>
|
||||||
|
<li ng-class="{active: !isShowAttributes}"><a href=""
|
||||||
|
data-ng-click="showJsonEditor()">{{:: 'client-profiles-json-editor' | translate}}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div data-ng-show="showListing()">
|
||||||
|
<table class="datatable table table-striped table-bordered dataTable no-footer">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="kc-table-actions" colspan="3">
|
||||||
|
<div class="form-inline">
|
||||||
|
<div class="pull-right" data-ng-show="access.manageClients">
|
||||||
|
<button class="btn btn-default" data-ng-click="create()">
|
||||||
|
{{:: 'create' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th width="90%">{{:: 'name' | translate}}</th>
|
||||||
|
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="attribute in config.attributes">
|
||||||
|
<td><a href="" data-ng-click="edit(attribute)">{{attribute.name}}</a></td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="edit(attribute)">{{:: 'edit' | translate}}</td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="removeAttribute(attribute)">{{:: 'delete' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
|
||||||
|
data-ng-show="!isShowAttributes">
|
||||||
|
<filedset>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<div>
|
||||||
|
<textarea id="userProfileConfig" name="userProfileConfig" data-ng-model="rawConfig"
|
||||||
|
class="form-control ng-binding" rows="20"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="col-md-10">
|
||||||
|
<button class="btn btn-primary" data-ng-click="save()">{{:: 'save' | translate}}</button>
|
||||||
|
<button class="btn btn-default" data-ng-click="reset()">{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</filedset>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form class="form-horizontal" name="realmForm" novalidate kc-read-only="!access.manageRealm"
|
||||||
|
data-ng-show="currentAttribute != null">
|
||||||
|
<p/>
|
||||||
|
<legend expanded><span class="text">{{:: 'user.profile.attribute' | translate}} <b
|
||||||
|
data-ng-show="!isCreate">{{currentAttribute.name}}</b> {{:: 'configuration' | translate}}</span>
|
||||||
|
</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="currentAttribute.name">{{:: 'user.profile.attribute.name' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.name.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<input name="currentAttribute.name" data-ng-model="currentAttribute.name" id="currentAttribute.name"
|
||||||
|
type="text" class="form-control"
|
||||||
|
data-ng-readonly="!isCreate"
|
||||||
|
required/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="isRequired">{{:: 'user.profile.attribute.required' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.required.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input name="isRequired" data-ng-model="isRequired" id="isRequired" onoffswitch
|
||||||
|
on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<fieldset class="border-top">
|
||||||
|
<legend collapsed><span class="text">{{:: 'user.profile.attribute.permission' | translate}}</span></legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="canUserView">{{:: 'user.profile.attribute.canUserView' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.canUserView.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<input name="canUserView" data-ng-model="canUserView" id="canUserView" onoffswitch
|
||||||
|
on-text="{{:: 'onText' | translate}}"
|
||||||
|
off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="canAdminView">{{:: 'user.profile.attribute.canAdminView' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.canAdminView.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input name="canAdminView" data-ng-model="canAdminView" id="canAdminView" onoffswitch
|
||||||
|
on-text="{{:: 'onText' | translate}}"
|
||||||
|
off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="canUserEdit">{{:: 'user.profile.attribute.canUserEdit' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.canUserEdit.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input name="canUserEdit" data-ng-model="canUserEdit" id="canUserEdit" onoffswitch
|
||||||
|
on-text="{{:: 'onText' | translate}}"
|
||||||
|
off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="canAdminEdit">{{:: 'user.profile.attribute.canAdminEdit' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.canAdminEdit.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<input name="canAdminEdit" data-ng-model="canAdminEdit" id="canAdminEdit" onoffswitch
|
||||||
|
on-text="{{:: 'onText' | translate}}"
|
||||||
|
off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend collapsed><span class="text">{{:: 'user.profile.attribute.validation' | translate}}</span></legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-md-2 control-label" for="create-validator">{{:: 'user.profile.attribute.validation.add.validator' | translate}}</label>
|
||||||
|
<kc-tooltip>{{:: 'user.profile.attribute.validation.add.validator.tooltip' | translate}}</kc-tooltip>
|
||||||
|
<div class="col-sm-4">
|
||||||
|
<select id="create-validator" class="form-control" ng-model="newValidator"
|
||||||
|
ng-options="p.id for p in validatorProviders"
|
||||||
|
data-ng-change="selectValidator(newValidator)">
|
||||||
|
<option value="" disabled selected>{{:: 'user.profile.attribute.validation.add.validator' | translate}}...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<kc-component-config realm="realm" config="newValidator.config"
|
||||||
|
properties="newValidator.properties"></kc-component-config>
|
||||||
|
<p/>
|
||||||
|
<div class="form-group" data-ng-show="newValidator != null">
|
||||||
|
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||||
|
<button class="btn btn-primary" data-ng-click="addValidator(newValidator)">{{:: 'add' | translate}}</button>
|
||||||
|
<button class="btn btn-primary" data-ng-click="cancelAddValidator()">{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group col-sm-10">
|
||||||
|
<table class="datatable table table-striped table-bordered dataTable no-footer">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="20%">{{:: 'name' | translate}}</th>
|
||||||
|
<th>{{:: 'config' | translate}}</th>
|
||||||
|
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="(key, value) in currentAttribute.validations">
|
||||||
|
<td>{{key}}</td>
|
||||||
|
<td>{{value}}</td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="removeValidator(key)">{{:: 'delete' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr data-ng-show="!currentAttribute.validations || currentAttribute.validations.length == 0">
|
||||||
|
<td class="text-muted" colspan="3">{{:: 'user.profile.attribute.validation.no.validators' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend collapsed><span class="text">{{:: 'user.profile.attribute.annotation' | translate}}</span></legend>
|
||||||
|
<div class="form-group col-sm-10">
|
||||||
|
<table class="table table-striped table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{:: 'key' | translate}}</th>
|
||||||
|
<th>{{:: 'value' | translate}}</th>
|
||||||
|
<th>{{:: 'actions' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr ng-repeat="(key, value) in currentAttribute.annotations | toOrderedMapSortedByKey">
|
||||||
|
<td>{{key}}</td>
|
||||||
|
<td><input ng-model="currentAttribute.annotations[key]" class="form-control" type="text" name="{{key}}"
|
||||||
|
id="attribute-{{key}}"/></td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="removeAnnotation(key)">{{:: 'delete' | translate}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><input ng-model="newAnnotation.key" class="form-control" type="text" id="newAnnotationKey"/></td>
|
||||||
|
<td><input ng-model="newAnnotation.value" class="form-control" type="text" id="newAnnotationValue"/></td>
|
||||||
|
<td class="kc-action-cell" data-ng-click="addAnnotation()"
|
||||||
|
data-ng-disabled="!newAnnotation.key.length || !newAnnotation.value.length">{{:: 'add' | translate}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<div class="form-group">
|
||||||
|
<p/>
|
||||||
|
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||||
|
<button kc-save>{{:: 'save' | translate}}</button>
|
||||||
|
<button kc-reset>{{:: 'cancel' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<kc-menu></kc-menu>
|
|
@ -19,5 +19,6 @@
|
||||||
<a href="#/realms/{{realm.realm}}/client-policies/profiles">{{:: 'realm-tab-client-policies' | translate}}</a>
|
<a href="#/realms/{{realm.realm}}/client-policies/profiles">{{:: 'realm-tab-client-policies' | translate}}</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
|
<li ng-class="{active: path[2] == 'defense'}" data-ng-show="access.viewRealm"><a href="#/realms/{{realm.realm}}/defense/headers">{{:: 'realm-tab-security-defenses' | translate}}</a></li>
|
||||||
|
<li ng-class="{active: path[2] == 'user-profile'}" data-ng-show="access.viewRealm && serverInfo.featureEnabled('DECLARATIVE_USER_PROFILE')"><a href="#/realms/{{realm.realm}}/user-profile">{{:: 'realm-tab-user-profile' | translate}}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
47
themes/src/main/resources/theme/base/login/verify-profile.ftl
Executable file
47
themes/src/main/resources/theme/base/login/verify-profile.ftl
Executable file
|
@ -0,0 +1,47 @@
|
||||||
|
<#import "template.ftl" as layout>
|
||||||
|
<@layout.registrationLayout displayMessage=!messagesPerField.existsError('username','email','firstName','lastName'); section>
|
||||||
|
<#if section = "header">
|
||||||
|
${msg("loginProfileTitle")}
|
||||||
|
<#elseif section = "form">
|
||||||
|
<form id="kc-update-profile-form" class="${properties.kcFormClass!}" action="${url.loginAction}" method="post">
|
||||||
|
|
||||||
|
<#list profile.attributes as attribute>
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<div class="${properties.kcLabelWrapperClass!}">
|
||||||
|
<label for="${attribute.name}" class="${properties.kcLabelClass!}">${msg("${attribute.name}")}</label>
|
||||||
|
<#if attribute.required>*</#if>
|
||||||
|
</div>
|
||||||
|
<div class="${properties.kcInputWrapperClass!}">
|
||||||
|
<input type="text" id="${attribute.name}" name="${attribute.name}" value="${(attribute.value!'')}"
|
||||||
|
class="${properties.kcInputClass!}"
|
||||||
|
aria-invalid="<#if messagesPerField.existsError('${attribute.name}')>true</#if>"
|
||||||
|
<#if attribute.readOnly>disabled</#if>
|
||||||
|
/>
|
||||||
|
|
||||||
|
<#if messagesPerField.existsError('${attribute.name}')>
|
||||||
|
<span id="input-error-${attribute.name}" class="${properties.kcInputErrorMessageClass!}" aria-live="polite">
|
||||||
|
${kcSanitize(messagesPerField.get('${attribute.name}'))?no_esc}
|
||||||
|
</span>
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</#list>
|
||||||
|
|
||||||
|
<div class="${properties.kcFormGroupClass!}">
|
||||||
|
<div id="kc-form-options" class="${properties.kcFormOptionsClass!}">
|
||||||
|
<div class="${properties.kcFormOptionsWrapperClass!}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
|
||||||
|
<#if isAppInitiatedAction??>
|
||||||
|
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||||
|
<button class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" type="submit" name="cancel-aia" value="true" />${msg("doCancel")}</button>
|
||||||
|
<#else>
|
||||||
|
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonBlockClass!} ${properties.kcButtonLargeClass!}" type="submit" value="${msg("doSubmit")}" />
|
||||||
|
</#if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</#if>
|
||||||
|
</@layout.registrationLayout>
|
|
@ -118,4 +118,17 @@ infoMessage=By clicking 'Remove Access', you will remove granted permissions of
|
||||||
doDelete=Delete
|
doDelete=Delete
|
||||||
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
|
deleteAccountSummary=Deleting your account will erase all your data and log you out immediately.
|
||||||
deleteAccount=Delete Account
|
deleteAccount=Delete Account
|
||||||
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
deleteAccountWarning=This is irreversible. All your data will be permanently destroyed, and irretrievable.
|
||||||
|
|
||||||
|
error-invalid-value=''{0}'' has invalid value.
|
||||||
|
error-invalid-blank=Please specify value of ''{0}''.
|
||||||
|
error-empty=Please specify value of ''{0}''.
|
||||||
|
error-invalid-length=''{0}'' must have a length between {1} and {2}.
|
||||||
|
error-invalid-email=Invalid email address.
|
||||||
|
error-invalid-number=''{0}'' is invalid number.
|
||||||
|
error-number-out-of-range=''{0}'' must be a number between {1} and {2}.
|
||||||
|
error-pattern-no-match=''{0}'' doesn''t match required format.
|
||||||
|
error-invalid-uri=''{0}'' is invalid URL.
|
||||||
|
error-invalid-uri-scheme=''{0}'' has invalid URL scheme.
|
||||||
|
error-invalid-uri-fragment=''{0}'' is invalid URL fragment.
|
||||||
|
error-user-attribute-required=Please specify ''{0}''.
|
|
@ -105,9 +105,13 @@ export class AccountServiceClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response !== null && response.data != null) {
|
if (response !== null && response.data != null) {
|
||||||
ContentAlert.danger(
|
if (response.data['errors'] != null) {
|
||||||
`${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`
|
for(let err of response.data['errors'])
|
||||||
);
|
ContentAlert.danger(err['errorMessage'], err['params']);
|
||||||
|
} else {
|
||||||
|
ContentAlert.danger(
|
||||||
|
`${response.statusText}: ${response.data['errorMessage'] ? response.data['errorMessage'] : ''} ${response.data['error'] ? response.data['error'] : ''}`);
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
ContentAlert.danger(response.statusText);
|
ContentAlert.danger(response.statusText);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue