KEYCLOAK-11770 add an hardcoded attribute mapper (#6396)

Signed-off-by: Cédric Couralet <cedric.couralet@insee.fr>
This commit is contained in:
Cédric Couralet 2019-12-10 12:57:46 +01:00 committed by Marek Posolda
parent 56aa14ffab
commit bde94f2f08
6 changed files with 390 additions and 30 deletions

View file

@ -17,12 +17,26 @@
package org.keycloak.storage.ldap;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.directory.SearchControls;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.models.utils.reflection.PropertyCriteria;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
@ -32,14 +46,6 @@ import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.membership.MembershipType;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.directory.SearchControls;
/**
* Allow to directly call some operations against LDAPIdentityStore.
*
@ -299,4 +305,34 @@ public class LDAPUtils {
}
}
}
/**
* Return a map of the user model properties from the getter methods
* Map key are the attributes names in lower case
*/
public static Map<String, Property<Object>> getUserModelProperties(){
Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class)
.addCriteria(new PropertyCriteria() {
@Override
public boolean methodMatches(Method m) {
if ((m.getName().startsWith("get") || m.getName().startsWith("is"))
&& m.getParameterTypes().length > 0) {
return false;
}
return true;
}
}).getResultList();
// Convert to be keyed by lower-cased attribute names
Map<String, Property<Object>> userModelProperties = new HashMap<>();
for (Map.Entry<String, Property<Object>> entry : userModelProps.entrySet()) {
userModelProperties.put(entry.getKey().toLowerCase(), entry.getValue());
}
return userModelProperties;
}
}

View file

@ -0,0 +1,133 @@
/*
* Copyright 2017 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 java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import org.keycloak.models.utils.reflection.Property;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
public class HardcodedAttributeMapper extends AbstractLDAPStorageMapper {
private static final Logger logger = Logger.getLogger(HardcodedAttributeMapper.class);
public HardcodedAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
super(mapperModel, ldapProvider);
}
private static final Map<String, Property<Object>> userModelProperties = LDAPUtils.getUserModelProperties();
public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
public static final String ATTRIBUTE_VALUE = "attribute.value";
@Override
public void onImportUserFromLDAP(LDAPObject ldapUser, UserModel user, RealmModel realm, boolean isCreate) {
String userModelAttrName = getUserModelAttribute();
String attributeValue = getAttributeValue();
Property<Object> userModelProperty = userModelProperties.get(userModelAttrName.toLowerCase());
if (userModelProperty != null) {
setPropertyOnUserModel(userModelProperty, user, attributeValue);
} else {
user.setAttribute(userModelAttrName, Arrays.asList(attributeValue));
}
}
@Override
public void onRegisterUserToLDAP(LDAPObject ldapUser, UserModel localUser, RealmModel realm) {
}
@Override
public UserModel proxy(final LDAPObject ldapUser, UserModel delegate, RealmModel realm) {
String userModelAttrName = getUserModelAttribute();
String attributeValue = getAttributeValue();
delegate = new UserModelDelegate(delegate) {
@Override
public List<String> getAttribute(String name) {
if(userModelAttrName.equals(name)){
return Arrays.asList(attributeValue);
}
return super.getAttribute(name);
}
@Override
public boolean isEmailVerified() {
if(userModelAttrName.equals("emailVerified")){
return Boolean.valueOf(attributeValue);
}
return super.isEmailVerified();
}
@Override
public boolean isEnabled() {
if(userModelAttrName.equals("enabled")){
return Boolean.valueOf(attributeValue);
}
return super.isEnabled();
}
};
return delegate;
}
private String getUserModelAttribute() {
return mapperModel.getConfig().getFirst(USER_MODEL_ATTRIBUTE);
}
String getAttributeValue() {
return mapperModel.getConfig().getFirst(ATTRIBUTE_VALUE);
}
protected void setPropertyOnUserModel(Property<Object> userModelProperty, UserModel user, String ldapAttrValue) {
if (ldapAttrValue == null) {
userModelProperty.setValue(user, null);
} else {
Class<Object> clazz = userModelProperty.getJavaClass();
if (String.class.equals(clazz)) {
userModelProperty.setValue(user, ldapAttrValue);
} else if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) {
Boolean boolVal = Boolean.valueOf(ldapAttrValue);
userModelProperty.setValue(user, boolVal);
} else {
logger.warnf("Don't know how to set the property '%s' on user '%s' . Value of LDAP attribute is '%s' ", userModelProperty.getName(), user.getUsername(), ldapAttrValue.toString());
}
}
}
@Override
public void beforeLDAPQuery(LDAPQuery query) {
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2017 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 java.util.ArrayList;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.provider.ConfigurationValidationHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.storage.ldap.LDAPStorageProvider;
public class HardcodedAttributeMapperFactory extends AbstractLDAPStorageMapperFactory {
public static final String PROVIDER_ID = "hardcoded-attribute-mapper";
protected static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty attrName = createConfigProperty(HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "User Model Attribute Name",
"Name of the model attribute, which will be added when importing user from ldap",
ProviderConfigProperty.STRING_TYPE, null);
ProviderConfigProperty attrValue = createConfigProperty(HardcodedAttributeMapper.ATTRIBUTE_VALUE, "Attribute Value",
"Value of the model attribute, which will be added when importing user from ldap.",
ProviderConfigProperty.STRING_TYPE, null);
configProperties.add(attrName);
configProperties.add(attrValue);
}
@Override
public String getHelpText() {
return "This mapper will hardcode any model user attribute and some property (like emailVerified or enabled) when importing user from ldap.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config) throws ComponentValidationException {
ConfigurationValidationHelper.check(config)
.checkRequired(HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "Attribute Name")
.checkRequired(HardcodedAttributeMapper.ATTRIBUTE_VALUE, "Attribute Value");
if(config.get(HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE).equalsIgnoreCase("username") || config.get(HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE).equalsIgnoreCase("email")){
throw new ComponentValidationException("Attribute Name cannot be set to username or email");
}
}
@Override
protected AbstractLDAPStorageMapper createMapper(ComponentModel mapperModel, LDAPStorageProvider federationProvider) {
return new HardcodedAttributeMapper(mapperModel, federationProvider);
}
}

View file

@ -31,9 +31,11 @@ import org.keycloak.models.utils.reflection.PropertyCriteria;
import org.keycloak.models.utils.reflection.PropertyQueries;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.LDAPUtils;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPUtil;
import java.lang.reflect.Method;
import java.util.ArrayList;
@ -53,28 +55,7 @@ public class UserAttributeLDAPStorageMapper extends AbstractLDAPStorageMapper {
private static final Logger logger = Logger.getLogger(UserAttributeLDAPStorageMapper.class);
private static final Map<String, Property<Object>> userModelProperties;
static {
Map<String, Property<Object>> userModelProps = PropertyQueries.createQuery(UserModel.class).addCriteria(new PropertyCriteria() {
@Override
public boolean methodMatches(Method m) {
if ((m.getName().startsWith("get") || m.getName().startsWith("is")) && m.getParameterTypes().length > 0) {
return false;
}
return true;
}
}).getResultList();
// Convert to be keyed by lower-cased attribute names
userModelProperties = new HashMap<>();
for (Map.Entry<String, Property<Object>> entry : userModelProps.entrySet()) {
userModelProperties.put(entry.getKey().toLowerCase(), entry.getValue());
}
}
private static final Map<String, Property<Object>> userModelProperties = LDAPUtils.getUserModelProperties();
public static final String USER_MODEL_ATTRIBUTE = "user.model.attribute";
public static final String LDAP_ATTRIBUTE = "ldap.attribute";

View file

@ -18,6 +18,7 @@
org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory
org.keycloak.storage.ldap.mappers.HardcodedLDAPRoleStorageMapperFactory
org.keycloak.storage.ldap.mappers.HardcodedLDAPGroupStorageMapperFactory
org.keycloak.storage.ldap.mappers.HardcodedAttributeMapperFactory
org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory
org.keycloak.storage.ldap.mappers.membership.group.GroupLDAPStorageMapperFactory
org.keycloak.storage.ldap.mappers.membership.role.RoleLDAPStorageMapperFactory

View file

@ -0,0 +1,126 @@
/*
* Copyright 2017 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.ldap;
import static org.keycloak.testsuite.arquillian.DeploymentTargetModifier.AUTH_SERVER_CURRENT;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Matcher;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.internal.matchers.ThrowableCauseMatcher;
import org.junit.rules.ExpectedException;
import org.junit.runners.MethodSorters;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.component.ComponentModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.mappers.HardcodedAttributeMapper;
import org.keycloak.storage.ldap.mappers.HardcodedAttributeMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.runonserver.RunOnServerException;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.testsuite.util.LDAPTestUtils;
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPHardcodedAttributeTest extends AbstractLDAPTest {
@ClassRule
public static LDAPRule ldapRule = new LDAPRule();
@Rule
public ExpectedException exceptionRule = ExpectedException.none();
@Deployment
@TargetsContainer(AUTH_SERVER_CURRENT)
public static WebArchive deploy() {
return RunOnServerDeployment.create(UserResource.class, AbstractLDAPTest.class).addPackages(true,
"org.keycloak.testsuite", "org.keycloak.testsuite.federation.ldap");
}
@Override
protected LDAPRule getLDAPRule() {
return ldapRule;
}
@Override
protected void afterImportTestRealm() {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
ComponentModel localeMapperModel = KeycloakModelUtils.createComponentModel("localeMapper", ctx.getLdapModel().getId(), HardcodedAttributeMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "locale",
HardcodedAttributeMapper.ATTRIBUTE_VALUE, "en");
ComponentModel emailVerifiedMapperModel = KeycloakModelUtils.createComponentModel("emailVerifiedMapper", ctx.getLdapModel().getId(), HardcodedAttributeMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "emailVerified",
HardcodedAttributeMapper.ATTRIBUTE_VALUE, "true");
appRealm.addComponentModel(localeMapperModel);
appRealm.addComponentModel(emailVerifiedMapperModel);
// Delete all LDAP users and add some new for testing
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe",
"john@email.org", null, "1234");
});
}
@Test
public void testHarcodedMapper(){
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
UserModel user = session.users().getUserByUsername("johnkeycloak", appRealm);
Assert.assertNotNull(user);
Assert.assertTrue(user.isEmailVerified());
Assert.assertEquals("en", user.getFirstAttribute("locale"));
});
}
@Test
public void testConfigInvalid(){
exceptionRule.expect(RunOnServerException.class);
exceptionRule.expectMessage("Attribute Name cannot be set to username or email");
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
ComponentModel usernameMapperModel = KeycloakModelUtils.createComponentModel("usernameMapper", ctx.getLdapModel().getId(), HardcodedAttributeMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
HardcodedAttributeMapper.USER_MODEL_ATTRIBUTE, "username",
HardcodedAttributeMapper.ATTRIBUTE_VALUE, "username");
appRealm.addComponentModel(usernameMapperModel);
});
}
}