Merge pull request #3642 from mposolda/master
KEYCLOAK-3921 LDAP binary attributes
This commit is contained in:
commit
0b701b3cf9
18 files changed
+655
-38
No files matched your search
@@ -45,6 +45,7 @@ postalCode: 77332
|
||||
postalCode: 66221
|
||||
street: Elm 5
|
||||
userPassword: password
|
||||
jpegPhoto:: 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 truncated
|
||||
|
||||
dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
|
||||
objectclass: top
|
||||
|
||||
@@ -119,6 +119,21 @@
|
||||
"id.token.claim" : "true",
|
||||
"access.token.claim" : "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"protocolMapper" : "oidc-usermodel-attribute-mapper",
|
||||
"protocol" : "openid-connect",
|
||||
"name" : "picture",
|
||||
"consentText" : "Picture",
|
||||
"consentRequired" : true,
|
||||
"config" : {
|
||||
"Claim JSON Type" : "String",
|
||||
"user.attribute" : "picture",
|
||||
"claim.name" : "profile_picture",
|
||||
"multivalued": "false",
|
||||
"id.token.claim" : "true",
|
||||
"access.token.claim" : "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -247,6 +262,19 @@
|
||||
"always.read.value.from.ldap" : "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name" : "picture",
|
||||
"federationMapperType" : "user-attribute-ldap-mapper",
|
||||
"federationProviderDisplayName" : "ldap-apacheds",
|
||||
"config" : {
|
||||
"ldap.attribute" : "jpegPhoto",
|
||||
"user.model.attribute" : "picture",
|
||||
"is.mandatory.in.ldap" : "false",
|
||||
"read.only" : "false",
|
||||
"always.read.value.from.ldap" : "true",
|
||||
"is.binary.attribute" : "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name" : "realm roles",
|
||||
"federationMapperType" : "role-ldap-mapper",
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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.example.ldap;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.representations.IDToken;
|
||||
|
||||
/**
|
||||
* Tests binary LDAP attribute
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class LDAPPictureServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
|
||||
resp.setContentType("image/jpeg");
|
||||
ServletOutputStream outputStream = resp.getOutputStream();
|
||||
|
||||
KeycloakSecurityContext securityContext = (KeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
|
||||
IDToken idToken = securityContext.getIdToken();
|
||||
|
||||
// TODO: Use idToken.getPicture() instead
|
||||
Object profilePicture = idToken.getOtherClaims().get("profile_picture");
|
||||
|
||||
if (profilePicture != null) {
|
||||
String base64EncodedPicture = getBase64EncodedPicture(profilePicture);
|
||||
byte[] decodedPicture = Base64.decode(base64EncodedPicture);
|
||||
outputStream.write(decodedPicture);
|
||||
}
|
||||
|
||||
outputStream.flush();
|
||||
}
|
||||
|
||||
private String getBase64EncodedPicture(Object profilePicture) {
|
||||
if (profilePicture instanceof List) {
|
||||
return ((List) profilePicture).get(0).toString();
|
||||
} else {
|
||||
return profilePicture.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,16 @@
|
||||
|
||||
<module-name>ldap-portal</module-name>
|
||||
|
||||
<servlet>
|
||||
<servlet-name>Picture</servlet-name>
|
||||
<servlet-class>org.keycloak.example.ldap.LDAPPictureServlet</servlet-class>
|
||||
</servlet>
|
||||
|
||||
<servlet-mapping>
|
||||
<servlet-name>Picture</servlet-name>
|
||||
<url-pattern>/picture/*</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>LDAPApp</web-resource-name>
|
||||
|
||||
@@ -43,9 +43,15 @@
|
||||
<h2>ID Token - other claims</h2>
|
||||
<%
|
||||
for (Map.Entry<String, Object> claim : idToken.getOtherClaims().entrySet()) {
|
||||
if (!claim.getKey().equals("profile_picture")) {
|
||||
%>
|
||||
<p><b><%= claim.getKey() %>: </b><%= claim.getValue().toString() %>
|
||||
<%
|
||||
} else {
|
||||
%>
|
||||
<p><b>Profile picture: </b><img src="/ldap-portal/picture" />
|
||||
<%
|
||||
}
|
||||
}
|
||||
%>
|
||||
<hr />
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.keycloak.storage.UserStorageProvider;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -34,6 +35,7 @@ import java.util.Set;
|
||||
public class LDAPConfig {
|
||||
|
||||
private final MultivaluedHashMap<String, String> config;
|
||||
private final Set<String> binaryAttributeNames = new HashSet<>();
|
||||
|
||||
public LDAPConfig(MultivaluedHashMap<String, String> config) {
|
||||
this.config = config;
|
||||
@@ -184,4 +186,39 @@ public class LDAPConfig {
|
||||
return UserStorageProvider.EditMode.valueOf(editModeString);
|
||||
}
|
||||
}
|
||||
|
||||
public void addBinaryAttribute(String attrName) {
|
||||
binaryAttributeNames.add(attrName);
|
||||
}
|
||||
|
||||
public Set<String> getBinaryAttributeNames() {
|
||||
return binaryAttributeNames;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) return true;
|
||||
if (!(obj instanceof LDAPConfig)) return false;
|
||||
|
||||
LDAPConfig that = (LDAPConfig) obj;
|
||||
|
||||
if (!config.equals(that.config)) return false;
|
||||
if (!binaryAttributeNames.equals(that.binaryAttributeNames)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return config.hashCode() * 13 + binaryAttributeNames.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(config);
|
||||
copy.remove(LDAPConstants.BIND_CREDENTIAL);
|
||||
return new StringBuilder(copy.toString())
|
||||
.append(", binaryAttributes: ").append(binaryAttributeNames)
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
+24
-21
@@ -20,8 +20,8 @@ package org.keycloak.storage.ldap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -33,37 +33,40 @@ public class LDAPIdentityStoreRegistry {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(LDAPIdentityStoreRegistry.class);
|
||||
|
||||
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<String, LDAPIdentityStoreContext>();
|
||||
private Map<String, LDAPIdentityStoreContext> ldapStores = new ConcurrentHashMap<>();
|
||||
|
||||
public LDAPIdentityStore getLdapStore(ComponentModel model) {
|
||||
LDAPIdentityStoreContext context = ldapStores.get(model.getId());
|
||||
public LDAPIdentityStore getLdapStore(ComponentModel ldapModel, Map<ComponentModel, LDAPConfigDecorator> configDecorators) {
|
||||
LDAPIdentityStoreContext context = ldapStores.get(ldapModel.getId());
|
||||
|
||||
// Ldap config might have changed for the realm. In this case, we must re-initialize
|
||||
MultivaluedHashMap<String, String> config = model.getConfig();
|
||||
if (context == null || !config.equals(context.config)) {
|
||||
logLDAPConfig(model.getName(), config);
|
||||
MultivaluedHashMap<String, String> configModel = ldapModel.getConfig();
|
||||
LDAPConfig ldapConfig = new LDAPConfig(configModel);
|
||||
for (Map.Entry<ComponentModel, LDAPConfigDecorator> entry : configDecorators.entrySet()) {
|
||||
ComponentModel mapperModel = entry.getKey();
|
||||
LDAPConfigDecorator decorator = entry.getValue();
|
||||
|
||||
LDAPIdentityStore store = createLdapIdentityStore(config);
|
||||
context = new LDAPIdentityStoreContext(config, store);
|
||||
ldapStores.put(model.getId(), context);
|
||||
decorator.updateLDAPConfig(ldapConfig, mapperModel);
|
||||
}
|
||||
|
||||
if (context == null || !ldapConfig.equals(context.config)) {
|
||||
logLDAPConfig(ldapModel.getName(), ldapConfig);
|
||||
|
||||
LDAPIdentityStore store = createLdapIdentityStore(ldapConfig);
|
||||
context = new LDAPIdentityStoreContext(ldapConfig, store);
|
||||
ldapStores.put(ldapModel.getId(), context);
|
||||
}
|
||||
return context.store;
|
||||
}
|
||||
|
||||
// Don't log LDAP password
|
||||
private void logLDAPConfig(String fedProviderDisplayName, MultivaluedHashMap<String, String> ldapConfig) {
|
||||
MultivaluedHashMap<String, String> copy = new MultivaluedHashMap<String, String>(ldapConfig);
|
||||
copy.remove(LDAPConstants.BIND_CREDENTIAL);
|
||||
logger.infof("Creating new LDAP based partition manager for the Federation provider: " + fedProviderDisplayName + ", LDAP Configuration: " + copy);
|
||||
private void logLDAPConfig(String fedProviderDisplayName, LDAPConfig ldapConfig) {
|
||||
logger.infof("Creating new LDAP Store for the LDAP storage provider: '%s', LDAP Configuration: %s", fedProviderDisplayName, ldapConfig.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ldapConfig from realm
|
||||
* @return PartitionManager instance based on LDAP store
|
||||
* Create LDAPIdentityStore to be cached in the local registry
|
||||
*/
|
||||
public static LDAPIdentityStore createLdapIdentityStore(MultivaluedHashMap<String, String> ldapConfig) {
|
||||
LDAPConfig cfg = new LDAPConfig(ldapConfig);
|
||||
|
||||
public static LDAPIdentityStore createLdapIdentityStore(LDAPConfig cfg) {
|
||||
checkSystemProperty("com.sun.jndi.ldap.connect.pool.authentication", "none simple");
|
||||
checkSystemProperty("com.sun.jndi.ldap.connect.pool.initsize", "1");
|
||||
checkSystemProperty("com.sun.jndi.ldap.connect.pool.maxsize", "1000");
|
||||
@@ -84,12 +87,12 @@ public class LDAPIdentityStoreRegistry {
|
||||
|
||||
private class LDAPIdentityStoreContext {
|
||||
|
||||
private LDAPIdentityStoreContext(MultivaluedHashMap<String, String> config, LDAPIdentityStore store) {
|
||||
private LDAPIdentityStoreContext(LDAPConfig config, LDAPIdentityStore store) {
|
||||
this.config = config;
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
private MultivaluedHashMap<String, String> config;
|
||||
private LDAPConfig config;
|
||||
private LDAPIdentityStore store;
|
||||
}
|
||||
}
|
||||
+25
-1
@@ -48,7 +48,9 @@ import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder;
|
||||
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
|
||||
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
|
||||
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
|
||||
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
|
||||
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory;
|
||||
import org.keycloak.storage.ldap.mappers.msad.MSADUserAccountControlStorageMapperFactory;
|
||||
@@ -57,7 +59,9 @@ import org.keycloak.storage.user.SynchronizationResult;
|
||||
import org.keycloak.utils.CredentialHelper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@@ -179,10 +183,30 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||
|
||||
@Override
|
||||
public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) {
|
||||
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model);
|
||||
Map<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
|
||||
|
||||
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(model, configDecorators);
|
||||
return new LDAPStorageProvider(this, session, model, ldapIdentityStore);
|
||||
}
|
||||
|
||||
|
||||
// Check if it's some performance overhead to create this map in every request. But probably not...
|
||||
protected Map<ComponentModel, LDAPConfigDecorator> getLDAPConfigDecorators(KeycloakSession session, ComponentModel ldapModel) {
|
||||
RealmModel realm = session.realms().getRealm(ldapModel.getParentId());
|
||||
List<ComponentModel> mapperComponents = realm.getComponents(ldapModel.getId(), LDAPStorageMapper.class.getName());
|
||||
|
||||
Map<ComponentModel, LDAPConfigDecorator> result = new HashMap<>();
|
||||
for (ComponentModel mapperModel : mapperComponents) {
|
||||
LDAPStorageMapperFactory mapperFactory = (LDAPStorageMapperFactory) session.getKeycloakSessionFactory().getProviderFactory(LDAPStorageMapper.class, mapperModel.getProviderId());
|
||||
if (mapperFactory instanceof LDAPConfigDecorator) {
|
||||
result.put(mapperModel, (LDAPConfigDecorator) mapperFactory);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
|
||||
LDAPConfig cfg = new LDAPConfig(config.getConfig());
|
||||
|
||||
+24
-3
@@ -18,6 +18,7 @@
|
||||
package org.keycloak.storage.ldap.idm.store.ldap;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.Base64;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.ModelException;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
@@ -40,6 +41,8 @@ import javax.naming.directory.DirContext;
|
||||
import javax.naming.directory.ModificationItem;
|
||||
import javax.naming.directory.SearchControls;
|
||||
import javax.naming.directory.SearchResult;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -323,8 +326,15 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||
Set<String> attrValues = new LinkedHashSet<>();
|
||||
NamingEnumeration<?> enumm = ldapAttribute.getAll();
|
||||
while (enumm.hasMoreElements()) {
|
||||
String attrVal = enumm.next().toString().trim();
|
||||
attrValues.add(attrVal);
|
||||
Object val = enumm.next();
|
||||
|
||||
if (val instanceof byte[]) { // byte[]
|
||||
String attrVal = Base64.encodeBytes((byte[]) val);
|
||||
attrValues.add(attrVal);
|
||||
} else { // String
|
||||
String attrVal = val.toString().trim();
|
||||
attrValues.add(attrVal);
|
||||
}
|
||||
}
|
||||
|
||||
if (ldapAttributeName.equalsIgnoreCase(LDAPConstants.OBJECT_CLASS)) {
|
||||
@@ -377,7 +387,18 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||
if (val == null || val.toString().trim().length() == 0) {
|
||||
val = LDAPConstants.EMPTY_ATTRIBUTE_VALUE;
|
||||
}
|
||||
attr.add(val);
|
||||
|
||||
if (getConfig().getBinaryAttributeNames().contains(attrName)) {
|
||||
// Binary attribute
|
||||
try {
|
||||
byte[] bytes = Base64.decode(val);
|
||||
attr.add(bytes);
|
||||
} catch (IOException ioe) {
|
||||
logger.warnf("Wasn't able to Base64 decode the attribute value. Ignoring attribute update. LDAP DN: %s, Attribute: %s, Attribute value: %s" + ldapObject.getDn(), attrName, attrValue);
|
||||
}
|
||||
} else {
|
||||
attr.add(val);
|
||||
}
|
||||
}
|
||||
|
||||
entryAttributes.put(attr);
|
||||
|
||||
+10
-1
@@ -515,8 +515,17 @@ public class LDAPOperationManager {
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder binaryAttrsBuilder = new StringBuilder();
|
||||
if (this.config.isObjectGUID()) {
|
||||
env.put("java.naming.ldap.attributes.binary", LDAPConstants.OBJECT_GUID);
|
||||
binaryAttrsBuilder.append(LDAPConstants.OBJECT_GUID).append(" ");
|
||||
}
|
||||
for (String attrName : config.getBinaryAttributeNames()) {
|
||||
binaryAttrsBuilder.append(attrName).append(" ");
|
||||
}
|
||||
|
||||
String binaryAttrs = binaryAttrsBuilder.toString().trim();
|
||||
if (!binaryAttrs.isEmpty()) {
|
||||
env.put("java.naming.ldap.attributes.binary", binaryAttrs);
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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.storage.ldap.mappers;
|
||||
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.storage.ldap.LDAPConfig;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface LDAPConfigDecorator {
|
||||
|
||||
void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel);
|
||||
|
||||
}
|
||||
+66
-9
@@ -80,6 +80,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
public static final String READ_ONLY = "read.only";
|
||||
public static final String ALWAYS_READ_VALUE_FROM_LDAP = "always.read.value.from.ldap";
|
||||
public static final String IS_MANDATORY_IN_LDAP = "is.mandatory.in.ldap";
|
||||
public static final String IS_BINARY_ATTRIBUTE = "is.binary.attribute";
|
||||
|
||||
public UserAttributeLDAPStorageMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
||||
super(mapperModel, ldapProvider);
|
||||
@@ -90,6 +91,12 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
String userModelAttrName = mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
|
||||
String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
|
||||
// We won't update binary attributes to Keycloak DB. They might be too big
|
||||
boolean isBinaryAttribute = mapperModel.get(IS_BINARY_ATTRIBUTE, false);
|
||||
if (isBinaryAttribute) {
|
||||
return;
|
||||
}
|
||||
|
||||
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
|
||||
|
||||
if (userModelProperty != null) {
|
||||
@@ -177,6 +184,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
final String ldapAttrName = mapperModel.getConfig().getFirst(LDAP_ATTRIBUTE);
|
||||
boolean isAlwaysReadValueFromLDAP = parseBooleanParameter(mapperModel, ALWAYS_READ_VALUE_FROM_LDAP);
|
||||
final boolean isMandatoryInLdap = parseBooleanParameter(mapperModel, IS_MANDATORY_IN_LDAP);
|
||||
final boolean isBinaryAttribute = parseBooleanParameter(mapperModel, IS_BINARY_ATTRIBUTE);
|
||||
|
||||
// For writable mode, we want to propagate writing of attribute to LDAP as well
|
||||
if (ldapProvider.getEditMode() == UserStorageProvider.EditMode.WRITABLE && !isReadOnly()) {
|
||||
@@ -185,20 +193,23 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
|
||||
@Override
|
||||
public void setSingleAttribute(String name, String value) {
|
||||
setLDAPAttribute(name, value);
|
||||
super.setSingleAttribute(name, value);
|
||||
if (setLDAPAttribute(name, value)) {
|
||||
super.setSingleAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, List<String> values) {
|
||||
setLDAPAttribute(name, values);
|
||||
super.setAttribute(name, values);
|
||||
if (setLDAPAttribute(name, values)) {
|
||||
super.setAttribute(name, values);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name) {
|
||||
setLDAPAttribute(name, null);
|
||||
super.removeAttribute(name);
|
||||
if ( setLDAPAttribute(name, null)) {
|
||||
super.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -221,10 +232,10 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
super.setFirstName(firstName);
|
||||
}
|
||||
|
||||
protected void setLDAPAttribute(String modelAttrName, Object value) {
|
||||
protected boolean setLDAPAttribute(String modelAttrName, Object value) {
|
||||
if (modelAttrName.equalsIgnoreCase(userModelAttrName)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
|
||||
if (UserAttributeLDAPStorageMapper.logger.isTraceEnabled()) {
|
||||
UserAttributeLDAPStorageMapper.logger.tracef("Pushing user attribute to LDAP. username: %s, Model attribute name: %s, LDAP attribute name: %s, Attribute value: %s", getUsername(), modelAttrName, ldapAttrName, value);
|
||||
}
|
||||
|
||||
ensureTransactionStarted();
|
||||
@@ -245,7 +256,53 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
|
||||
ldapUser.setAttribute(ldapAttrName, new LinkedHashSet<>(asList));
|
||||
}
|
||||
}
|
||||
|
||||
if (isBinaryAttribute) {
|
||||
UserAttributeLDAPStorageMapper.logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute.", userModelAttrName, getUsername());
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
} else if (isBinaryAttribute) {
|
||||
|
||||
delegate = new UserModelDelegate(delegate) {
|
||||
|
||||
@Override
|
||||
public void setSingleAttribute(String name, String value) {
|
||||
if (name.equalsIgnoreCase(userModelAttrName)) {
|
||||
logSkipDBWrite();
|
||||
} else {
|
||||
super.setSingleAttribute(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(String name, List<String> values) {
|
||||
if (name.equalsIgnoreCase(userModelAttrName)) {
|
||||
logSkipDBWrite();
|
||||
} else {
|
||||
super.setAttribute(name, values);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAttribute(String name) {
|
||||
if (name.equalsIgnoreCase(userModelAttrName)) {
|
||||
logSkipDBWrite();
|
||||
} else {
|
||||
super.removeAttribute(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void logSkipDBWrite() {
|
||||
logger.debugf("Skip writing model attribute '%s' to DB for user '%s' as it is mapped to binary LDAP attribute", userModelAttrName, getUsername());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
+21
-1
@@ -32,7 +32,7 @@ import java.util.List;
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory {
|
||||
public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMapperFactory implements LDAPConfigDecorator {
|
||||
|
||||
public static final String PROVIDER_ID = "user-attribute-ldap-mapper";
|
||||
protected static final List<ProviderConfigProperty> configProperties;
|
||||
@@ -69,6 +69,10 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
||||
.helpText("If true, attribute is mandatory in LDAP. Hence if there is no value in Keycloak DB, the empty value will be set to be propagated to LDAP")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add()
|
||||
.property().name(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE).label("Is Binary Attribute")
|
||||
.helpText("Should be true for binary LDAP attributes")
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false").add()
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -92,6 +96,12 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
||||
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute", config);
|
||||
checkMandatoryConfigAttribute(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, "LDAP Attribute", config);
|
||||
|
||||
boolean isBinaryAttribute = config.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
|
||||
boolean alwaysReadValueFromLDAP = config.get(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, false);
|
||||
if (isBinaryAttribute && !alwaysReadValueFromLDAP) {
|
||||
throw new ComponentValidationException("With Binary attribute enabled, the ''Always read value from LDAP'' must be enabled too");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -103,4 +113,14 @@ public class UserAttributeLDAPStorageMapperFactory extends AbstractLDAPStorageMa
|
||||
public List<ProviderConfigProperty> getConfigProperties(RealmModel realm, ComponentModel parent) {
|
||||
return getConfigProps(parent);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void updateLDAPConfig(LDAPConfig ldapConfig, ComponentModel mapperModel) {
|
||||
boolean isBinaryAttribute = mapperModel.get(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, false);
|
||||
if (isBinaryAttribute) {
|
||||
String ldapAttrName = mapperModel.getConfig().getFirst(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE);
|
||||
ldapConfig.addBinaryAttribute(ldapAttrName);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ public class LDAPConstants {
|
||||
public static final String OBJECT_CLASS = "objectclass";
|
||||
public static final String UID = "uid";
|
||||
public static final String USER_PASSWORD_ATTRIBUTE = "userpassword";
|
||||
public static final String JPEG_PHOTO = "jpegPhoto";
|
||||
public static final String GROUP = "group";
|
||||
public static final String GROUP_OF_NAMES = "groupOfNames";
|
||||
public static final String GROUP_OF_ENTRIES = "groupOfEntries";
|
||||
|
||||
@@ -182,8 +182,10 @@ public class UsersResource {
|
||||
} catch (ModelReadOnlyException re) {
|
||||
return ErrorResponse.exists("User is read only!");
|
||||
} catch (ModelException me) {
|
||||
logger.warn("Could not update user!", me);
|
||||
return ErrorResponse.exists("Could not update user!");
|
||||
} catch (Exception me) { // JPA may be committed by JTA which can't
|
||||
} catch (Exception me) { // JPA
|
||||
logger.warn("Could not update user!", me);// may be committed by JTA which can't
|
||||
return ErrorResponse.exists("Could not update user!");
|
||||
}
|
||||
}
|
||||
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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.federation.storage.ldap;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.ClientErrorException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.FixMethodOrder;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.RuleChain;
|
||||
import org.junit.rules.TestRule;
|
||||
import org.junit.runners.MethodSorters;
|
||||
import org.keycloak.admin.client.Keycloak;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.UserModelDelegate;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.services.managers.RealmManager;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
import org.keycloak.storage.UserStorageProviderModel;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProvider;
|
||||
import org.keycloak.storage.ldap.LDAPStorageProviderFactory;
|
||||
import org.keycloak.storage.ldap.LDAPUtils;
|
||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||
import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper;
|
||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||
import org.keycloak.testsuite.rule.LDAPRule;
|
||||
|
||||
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.MASTER;
|
||||
import static org.keycloak.models.AdminRoles.ADMIN;
|
||||
import static org.keycloak.testsuite.Constants.AUTH_SERVER_ROOT;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||
public class LDAPBinaryAttributesTest {
|
||||
|
||||
private static LDAPRule ldapRule = new LDAPRule();
|
||||
|
||||
private static ComponentModel ldapModel = null;
|
||||
|
||||
|
||||
private static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
|
||||
|
||||
@Override
|
||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||
MultivaluedHashMap<String,String> ldapConfig = LDAPTestUtils.getLdapRuleConfig(ldapRule);
|
||||
ldapConfig.putSingle(LDAPConstants.SYNC_REGISTRATIONS, "true");
|
||||
ldapConfig.putSingle(LDAPConstants.EDIT_MODE, UserStorageProvider.EditMode.WRITABLE.toString());
|
||||
UserStorageProviderModel model = new UserStorageProviderModel();
|
||||
model.setLastSync(0);
|
||||
model.setChangedSyncPeriod(-1);
|
||||
model.setFullSyncPeriod(-1);
|
||||
model.setName("test-ldap");
|
||||
model.setPriority(0);
|
||||
model.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME);
|
||||
model.setConfig(ldapConfig);
|
||||
|
||||
ldapModel = appRealm.addComponentModel(model);
|
||||
LDAPTestUtils.addZipCodeLDAPMapper(appRealm, ldapModel);
|
||||
|
||||
// Delete all LDAP users and add some new for testing
|
||||
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
|
||||
LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
|
||||
|
||||
// LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
|
||||
// LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
|
||||
//
|
||||
// LDAPObject existing = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "existing", "Existing", "Foo", "existing@email.org", null, "5678");
|
||||
|
||||
appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
|
||||
}
|
||||
});
|
||||
|
||||
@ClassRule
|
||||
public static TestRule chain = RuleChain
|
||||
.outerRule(ldapRule)
|
||||
.around(keycloakRule);
|
||||
|
||||
private static final String JPEG_PHOTO_BASE64 = "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 truncated
|
||||
|
||||
|
||||
|
||||
protected Keycloak adminClient;
|
||||
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
adminClient = Keycloak.getInstance(AUTH_SERVER_ROOT, MASTER, ADMIN, ADMIN, Constants.ADMIN_CLI_CLIENT_ID);
|
||||
}
|
||||
|
||||
@After
|
||||
public void after() {
|
||||
ComponentRepresentation jpegMapper = adminClient.realm("test").components().query(ldapModel.getId(), LDAPStorageMapper.class.getName(), "jpeg-mapper").get(0);
|
||||
adminClient.realm("test").components().component(jpegMapper.getId()).remove();
|
||||
|
||||
adminClient.close();
|
||||
}
|
||||
|
||||
|
||||
// Test invalid mapper configuration - validation exception thrown
|
||||
@Test
|
||||
public void test01InvalidMapperConfiguration() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
|
||||
ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
|
||||
|
||||
ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
|
||||
try {
|
||||
appRealm.updateComponent(ldapComponentMapper);
|
||||
Assert.fail("Not expected to successfully update mapper");
|
||||
} catch (ComponentValidationException cve) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test02ReadOnly() {
|
||||
String mapperId = addPhotoMapper();
|
||||
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
|
||||
// Add user directly to LDAP
|
||||
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ldapModel);
|
||||
addLDAPUser(ldapFedProvider, appRealm, "johnphoto", "John", "Photo", "john@photo.org", JPEG_PHOTO_BASE64);
|
||||
|
||||
// Set mapper to be read-only
|
||||
ComponentModel ldapComponentMapper = appRealm.getComponent(mapperId);
|
||||
ldapComponentMapper.put(UserAttributeLDAPStorageMapper.READ_ONLY, true);
|
||||
appRealm.updateComponent(ldapComponentMapper);
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
|
||||
// Assert john found
|
||||
getUserAndAssertPhoto("johnphoto", true);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void test03WritableMapper() {
|
||||
String mapperId = addPhotoMapper();
|
||||
|
||||
// Create user joe with jpegPHoto
|
||||
UserRepresentation joe = new UserRepresentation();
|
||||
joe.setUsername("joephoto");
|
||||
joe.setEmail("joe@photo.org");
|
||||
joe.setAttributes(Collections.singletonMap(LDAPConstants.JPEG_PHOTO, Arrays.asList(JPEG_PHOTO_BASE64)));
|
||||
Response response = adminClient.realm("test").users().create(joe);
|
||||
response.close();
|
||||
|
||||
|
||||
// Assert he is found including jpegPhoto
|
||||
joe = getUserAndAssertPhoto("joephoto", true);
|
||||
|
||||
|
||||
// Try to update him with some big non-LDAP mapped attribute. It will fail
|
||||
try {
|
||||
joe.getAttributes().put("someOtherPhoto", Arrays.asList(JPEG_PHOTO_BASE64));
|
||||
adminClient.realm("test").users().get(joe.getId()).update(joe);
|
||||
Assert.fail("Not expected to successfully update user");
|
||||
} catch (ClientErrorException cee) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
|
||||
// Try to update him with some bad value for jpegPhoto. It will fail
|
||||
try {
|
||||
joe.getAttributes().remove("someOtherPhoto");
|
||||
joe.getAttributes().put(LDAPConstants.JPEG_PHOTO, Arrays.asList("foobar"));
|
||||
adminClient.realm("test").users().get(joe.getId()).update(joe);
|
||||
Assert.fail("Not expected to successfully update user");
|
||||
} catch (ClientErrorException cee) {
|
||||
// Expected
|
||||
}
|
||||
|
||||
|
||||
// Remove jpegPhoto attribute and assert it was successfully removed
|
||||
joe.getAttributes().remove(LDAPConstants.JPEG_PHOTO);
|
||||
adminClient.realm("test").users().get(joe.getId()).update(joe);
|
||||
getUserAndAssertPhoto("joephoto", false);
|
||||
}
|
||||
|
||||
|
||||
private String addPhotoMapper() {
|
||||
KeycloakSession session = keycloakRule.startSession();
|
||||
try {
|
||||
RealmManager manager = new RealmManager(session);
|
||||
RealmModel appRealm = manager.getRealm("test");
|
||||
|
||||
ComponentModel ldapComponentMapper = LDAPTestUtils.addUserAttributeMapper(appRealm, ldapModel, "jpeg-mapper", LDAPConstants.JPEG_PHOTO, LDAPConstants.JPEG_PHOTO);
|
||||
|
||||
ldapComponentMapper.put(UserAttributeLDAPStorageMapper.IS_BINARY_ATTRIBUTE, true);
|
||||
ldapComponentMapper.put(UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, true);
|
||||
appRealm.updateComponent(ldapComponentMapper);
|
||||
return ldapComponentMapper.getId();
|
||||
} finally {
|
||||
keycloakRule.stopSession(session, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private LDAPObject addLDAPUser(LDAPStorageProvider ldapProvider, RealmModel realm, final String username,
|
||||
final String firstName, final String lastName, final String email, String jpegPhoto) {
|
||||
UserModel helperUser = new UserModelDelegate(null) {
|
||||
|
||||
@Override
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAttribute(String name) {
|
||||
if (LDAPConstants.JPEG_PHOTO.equals(name)) {
|
||||
return Arrays.asList(jpegPhoto);
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
};
|
||||
return LDAPUtils.addUserToLDAP(ldapProvider, realm, helperUser);
|
||||
}
|
||||
|
||||
|
||||
private UserRepresentation getUserAndAssertPhoto(String username, boolean isPhotoExpected) {
|
||||
List<UserRepresentation> johns = adminClient.realm("test").users().search(username, 0, 10);
|
||||
Assert.assertEquals(1, johns.size());
|
||||
UserRepresentation john = johns.get(0);
|
||||
Assert.assertEquals(username, john.getUsername());
|
||||
Assert.assertTrue(john.getAttributes().containsKey(LDAPConstants.LDAP_ID)); // Doublecheck it's the LDAP mapped user
|
||||
|
||||
if (isPhotoExpected) {
|
||||
Assert.assertEquals(JPEG_PHOTO_BASE64, john.getAttributes().get(LDAPConstants.JPEG_PHOTO).get(0));
|
||||
} else {
|
||||
Assert.assertFalse(john.getAttributes().containsKey(LDAPConstants.JPEG_PHOTO));
|
||||
}
|
||||
return john;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -78,4 +78,5 @@ log4j.logger.org.apache.directory.server.ldap.LdapProtocolHandler=error
|
||||
#log4j.logger.org.apache.http.impl.conn=debug
|
||||
|
||||
# Enable to view details from identity provider authenticator
|
||||
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
|
||||
# log4j.logger.org.keycloak.authentication.authenticators.browser.IdentityProviderAuthenticator=trace
|
||||
log4j.logger.org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper=debug
|
||||
@@ -45,6 +45,7 @@ postalCode: 88441
|
||||
postalCode: 77332
|
||||
street: Elm 5
|
||||
userPassword: password
|
||||
jpegPhoto:: /9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAMDAwMDAwQEBAQFBQUFBQcHBgYHBwsICQgJCAsRCwwLCwwLEQ8SDw4PEg8bFRMTFRsfGhkaHyYiIiYwLTA+PlQBAwMDAwMDBAQEBAUFBQUFBwcGBgcHCwgJCAkICxELDAsLDAsRDxIPDg8SDxsVExMVGx8aGRofJiIiJjAtMD4+VP/CABEIAFIAWAMBIgACEQEDEQH/xAAdAAACAgMBAQEAAAAAAAAAAAAGCAUHAwQJAgAB/9oACAEBAAAAAElmzK1aOaraUmpiktrD10DayAIMkKunPQdk+hrZwkUNkMrM88VDtt7r7C1KCJtprbSBP2J6K+VDqUlwErkDnOm/HF9IPDaWQ+cP85sXS7OCj1Iybjj2zVvJA0b91BqJ1JuQDYfkuSWrGdFzsMsWkaIUGHh4IuY1glqtWrK8G89YeJk+ldfT1pHz9//EABoBAAIDAQEAAAAAAAAAAAAAAAUHAwYIBAD/2gAIAQIQAAAApKICefJOj5haleI6vyhC1+FEa9UydaFaD7hDLtU9jttBsCJni6f/xAAaAQADAQEBAQAAAAAAAAAAAAAFBgcDBAII/9oACAEDEAAAAF5+Mdc4X6LUuz2kyGquiYYI/PzvUctR8d5289ghjS48fuOK/wD/xAA5EAABAwIEBAMGAwcFAAAAAAABAgMEABEFBhIhEzFBURQiMgdCYXGBwSOhsRUkM3KCkdFSU2J04f/aAAgBAQABPwGHLbkIVbmjmPhSF60BSL2VUPDhLYLjL4KuotyP0NQPZq5iWBmUiWPGFarIV/DIHS/MGpeC4lhrnh5MdxLgJBuna/wPWsFyW2/kVaH2/wB7dK5LSyPMDbb6KtWD+zqBKypKTIZQJ89q4dPNG10f+1GydizuAPSSgstwlHiawQVnVby/LrTrfBOkqF6gyWmEJQSoKvfVYUiawVbrubc6bQEr4qSkKpDj/iEMKSdLoDYSm/XkRWSco+Bg4dORxo8lCyJcd1NgqxI68tjSG2Wm/wAFKUi97DYVi2GRcZh8F+9goLSR0IpC02TakqA2rE4acSgPw9WgPJ0qPYE71mL2fQJLanYiOGmJh5DKEep125IualtvxJDrDo0OtLKFjsQd6U8tJ2686deDa/Ii1/STvWU8KwDOOVoaZ8VPioV2VLQdC0WNwQRSU2QEk3sNyetPExzq9zr8KEwpulXTrTWJx0OLQp5IOskC/Sm5iFpSpCr3A/S9IeF9zQJPI2r2g5Tw3AWFS0uPOPzpay3q9KE8zc9Tvzpxv6HrWUcCh5hnmC/iQiv6bskp18XvbcVlXJE3K0xbrOJJfadFnGi2U/UG5peocqxPEmYrCtak3I2FTcckBwp1q9PIck87Hbn3oLllYWTvzvUbFpkE7XIv1+FYFizOIM60W18inqmmeIrnXtTaxmW82p2I61CYFkL2Ui6upI5XrhKS0nWL6VadQ95J7VkzKcZzEor4lsHgkL4KwSrWO1ulJdWbeUHbcjaluqAPkX+VZjxMLkur07Nk7cjYdjyvTSzJcceWLFe4+9Mt6x5eQNOBCiR8enSsqylxcSLSdR1g7X273pmVYedJT9Kx7L+G5mjBp911sp9KkqOx+KTsazDl+Rl3EBHcktvNKuptSFbEfEdCKyEGHNi+OKhZUGFNg/1JJ3BoSU99/lTuzZUs2FutZhaSZT5SggJVcG29r9bU1p8OhQuRalyHgBa4TyriWTfqayjEck4kp1CiA0j63VyplTqPKtQV89qnfsl5jhzyzw1/7igP7XrPOF4fgmMNrg4ozIjSFH8BCwtbI7E9u1ZCwvCpjrcyRKQHW3PLG1WJ+N/8U65o3sBSWCs8R3c9B2rN2BGWy7ISQDpsRb86SuRC1IlNLQb9ep+B60ZUbYnaokOXijwaYQdJ5q90bd6wDBF4JESlJC7+Zdhvc00W3htWbZmHYVhi3J8PxUVR0rb06t+l+3zrFXMLU6VQUvJJcWVBQ0pSOiUi6jt3Jpryea52r2c+KxKap9995TEVvyjWbFajtfvWsL+VKW1iBJQoKZbV03uof4qThEWWyriNJWee++9R8uYTH06YrYIH+kU9BaCNKU2t6aw93iJ0KO6axfFIOFSW1OSEsFwn1GwJHOsdbYzhlybEgy21ugC4QoG+nfSfnU6MAtY9JR7vfferqPQmsOzTOw9bDMdTLZTfhtcklZFrnufnWLe0HM+JLMbi+FSfIpDW3zuedZXQzGyxh/IDw4Wf6/Mo1h8pqbCaktnyOi6T8L1PxlqFj0OE5sJLK7fzDkKOlbYVWM4yzl7FY63dmJKTqI90o62r2pQXnDExpg8aK4gNmytkHoR8DTMx6MVLYdW2u1uo/NNL1JSt3UFq08qCVN+pR2pS9SiR32oyPElp824iLIc/5dlUjOstvLyMKCAAEFBdJ9y/Kms2QMNyvBjofC3ww2opQb8/MAfvWPY/IxzFvFqUW9B/BsfSByFSfaGpzAm+E4WZrTyF2725/Q9qx7NT+ZHY7i20t8JrTpBvc33NSJkzgmKH3THJ1cLUdN/lQTtvUSO2664FXA0LWr6C9KJJNConrP8AIaRzXTPo/t+tO/f71J9X0H6U1T1L+1QNncQ/6n3Ff//EACQQAQEAAgICAQQDAQAAAAAAAAERACExQVGBYXGRobEQwfDR/9oACAEBAAE/EFxCJHa+QcvGUwhESkN9fiYHv0VpOm0o9OFKc/OP0nyMqyNS7O0x2FTnAH6HHF+oMkQP6cPbVCpqdHusdzBsTjjKJKrAZwN411hOmhN3Q+OdYFicQWN6QTJzrwEHC7aJwbcdwysS/AW7GNhVpxF5hwesFHQhHh77IyY3UAIHQHgwuHXP36x4apQUBQcWcXJmcwJakbelduaVYQKsBS3eAVJyB+cQnvn1w6o9ZDI6pnUIiPfdxG9iCBQRXreNy18f2PjFCeB0fH3MMVGwtGz1hMB2JKPunWGBO9cLh1omlDTJZj6GlAPSm3RMCgLAgSfWedY3okyFyEYQWneraZLkQPZNfjHeyDpVdBP7dYuNwjIibANNDHmgaDAeXRptkBMDpGVE6Hqjvltx4aCVkJq9o4gOA4y6pRz7bXiMrUKoBArI5ZdacH0tDvKBFDsjg3oRL8G/tceEiW1Y6UAXW0jxK47AXAL0gOg3HkF1MeStC2JfnHWoCStw/wDe804AlIhCOmmMAu7jZ+Fw0B2U6H2hTGAuDJHQLuYV1xK4W0jhBExMKEcCV+5m01yqkMRn8VrZVUN24oNBOeYF1eI6cEGRpvfJrB+GQLVd9ZIRJROhHod4eCYNA8PTGhag8kNPJivYOADUtVxNVsC44dbHxjhJe2AYOXuc16+cQODu1ej3TF3gwCyreivI5JUksX4fesSG1rUxOwRWcCuJY2pBRrL0AQyrY62UfZjjgyEKuhQpIYLK9dtHzfVC4V2Gib2vOsI+EqqQI4gFwY1Py185FplUDmj8LU84jKWQGhYb+GYoKQdBVa8fLgkXPHI8s+uWSNh+pzh6/EJ6CF0MduPBMS2w88OjhgEW0VUC+Ht6xbrYBeevOWtwmrpdV6RSGDx83G9WqMDRe3oFCj2uUoN4FoGLoFWobz7guMGnj2W4jCzVWD1RwXq8VTsWxxafUEV2R2WOBoHSEnGyczAFEcmv43hrndLdMA4GT4cJ8xi4mEFtpKDrTMB0WxjaWEKwJpfaeX95xkQjKxHJ5FgWWx4WjvAwAztJmx6tsUyaoEOUrgVl3jsSC8V1lgV0fwbzc/pzg9R17zkOp+jGh6Z5/wDGuf2M4nvOf+9ZxOos/wD/xAAvEQABAwMCBQMDAwUAAAAAAAACAQMEAAUREiEGBxMxQSJRcSMzYRQygRUXQpGy/9oACAECAQE/AHIDLhl0i0+VQvHvXF3MCTaZ7kW0voatv+o1FP8ABcKBfhavPM+dcbZESOCsSQki49pzj0dk+Fqy81r3FkSBkfUbkzAdQjVS6I9lQUqw8SWzi6GRRjVSHTrBdiHPaisAEuUNa5mXmBDgLFC4lBubGHmCRDFT8YQsYwtQmHpkpBFOorpepPfPmoPAIq0RPvCCGKYRKv3B71pZV8fqte6ePmuVEmaxxMLDUkWmDTL45RNenslA5lNiT/aVzPmzJt7EHzJ1GQwKKONOd65cxYz9wdUw3ANqfaOQaiC6UHbHxVxYaGzTlfX6aNL37ZqG49CurT0UnWzE8obKay/hPNWtXpdqhSFdV7qsoqmrStqq+cj4WuYPB5SVfu7twJWmAXDKhnT7COKslxnW6cMtvIg0qIW22Pav7i2hwOq7EdF3VujZJiuI+MnrzDVuO30owEmtvuq/lVrh+2G1dbZLfbkDBceDW4iEmlM99SVFVhY7e6l6e5KhLTtmjXAFafQTbwuyjsua454JC4223QLXHajNhJQnhBNO2MZ/NWLl0/Luk39Q2rcQeu2Cki6s9gJK4e5ZXNqUSzxT9KXWbcHOC2T0qlcPWz+k2dmC+YPi1kQXTjLefShJ7pTUiO2pAaIqpjG2MJQft/irl+9Ka+4XwtJ9qmu4/NF99z4D/mv/xAAsEQACAQMCBAUDBQAAAAAAAAABAgMABBEFEgYhMUETFCIycVFSYRUzNGJy/9oACAEDAQE/AE09bmQ+F8kNWhcIsrs9wAYmHp+vMVYcIx291cO+JInQqoPXJ61c8MWzW6Rx5HhoQP7HHKtZ0W5tMJMMhjkN8V5B/uFcM2EodZDAjxMNpY9qtoRyIxtHUVLNawYyBQEM6+nGcVxVFB5F96bmHsI7ZokZ61wdEP0pJcDdJzPqzSnZESprWricTAktWgtNtBfJJ7GtRtYZonWRAQQeXatdsXtr90VNoxyAbIrhDXbaJYrKO1IZurA5yfqajYk7GwAwqWxikOSVNJFHbx5BBP4q9uikEojdC/hnCEg1LNHM5Zwuc9xWnRywSq8UrJjmDitP4ha2neS6kYnwiFB+7NXLine truncated
|
||||
|
||||
dn: cn=ldap-user,ou=RealmRoles,dc=keycloak,dc=org
|
||||
objectclass: top
|
||||
|
||||
Reference in new issue
Block a user