KEYCLOAK-10927 - Implement LDAPv3 Password Modify Extended Operation … (#6962)

* KEYCLOAK-10927 - Implement LDAPv3 Password Modify Extended Operation (RFC-3062).

* KEYCLOAK-10927 - Introduce getLDAPSupportedExtensions(). Use result instead of configuration.

Co-authored-by: Lars Uffmann <lars.uffmann@vitroconnect.de>
Co-authored-by: Kevin Kappen <kevin.kappen@vitroconnect.de>
Co-authored-by: mposolda <mposolda@gmail.com>
This commit is contained in:
cachescrubber 2020-05-20 21:04:45 +02:00 committed by GitHub
parent cc776204f0
commit 3382682115
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 952 additions and 264 deletions

View file

@ -0,0 +1,95 @@
/*
* Copyright 2019 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.representations.idm;
import java.util.Objects;
import org.keycloak.common.util.ObjectUtil;
/**
* Value object to represent an OID (object identifier) as used to describe LDAP schema, extension and features.
* See <a href="https://ldap.com/ldap-oid-reference-guide/">LDAP OID Reference Guide</a>.
*
* @author Lars Uffmann, 2020-05-13
* @since 11.0
*/
public class LDAPCapabilityRepresentation {
public enum CapabilityType {
CONTROL,
EXTENSION,
FEATURE,
UNKNOWN;
public static CapabilityType fromRootDseAttributeName(String attributeName) {
switch (attributeName) {
case "supportedExtension": return CapabilityType.EXTENSION;
case "supportedControl": return CapabilityType.CONTROL;
case "supportedFeatures": return CapabilityType.FEATURE;
default: return CapabilityType.UNKNOWN;
}
}
};
private Object oid;
private CapabilityType type;
public LDAPCapabilityRepresentation() {
}
public LDAPCapabilityRepresentation(Object oidValue, CapabilityType type) {
this.oid = Objects.requireNonNull(oidValue);
this.type = type;
}
public String getOid() {
return oid instanceof String ? (String) oid : String.valueOf(oid);
}
public CapabilityType getType() {
return type;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
LDAPCapabilityRepresentation ldapOid = (LDAPCapabilityRepresentation) o;
return ObjectUtil.isEqualOrBothNull(oid, ldapOid.oid) && ObjectUtil.isEqualOrBothNull(type, ldapOid.type);
}
@Override
public int hashCode() {
return oid.hashCode();
}
@Override
public String toString() {
return new StringBuilder(LDAPCapabilityRepresentation.class.getSimpleName() + "[ ")
.append("oid=" + oid + ", ")
.append("type=" + type + " ]")
.toString();
}
}

View file

@ -10,17 +10,24 @@ public class TestLdapConnectionRepresentation {
private String connectionTimeout;
private String componentId;
private String startTls;
private String authType;
public TestLdapConnectionRepresentation() {
}
public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout) {
this(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, null, null);
}
public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout, String startTls, String authType) {
this.action = action;
this.connectionUrl = connectionUrl;
this.bindDn = bindDn;
this.bindCredential = bindCredential;
this.useTruststoreSpi = useTruststoreSpi;
this.connectionTimeout = connectionTimeout;
this.startTls = startTls;
this.authType = authType;
}
public String getAction() {
@ -39,6 +46,14 @@ public class TestLdapConnectionRepresentation {
this.connectionUrl = connectionUrl;
}
public String getAuthType() {
return authType;
}
public void setAuthType(String authType) {
this.authType = authType;
}
public String getBindDn() {
return bindDn;
}

View file

@ -60,6 +60,11 @@ public class LDAPConfig {
}
}
public boolean useExtendedPasswordModifyOp() {
String value = config.getFirst(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP);
return Boolean.parseBoolean(value);
}
public String getUseTruststoreSpi() {
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
}

View file

@ -49,6 +49,8 @@ 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.HardcodedLDAPAttributeMapper;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
@ -106,6 +108,9 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
.property().name(LDAPConstants.VENDOR)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.add()
.property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
@ -308,6 +313,7 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
UserStorageProvider.EditMode editMode = ldapConfig.getEditMode();
String readOnly = String.valueOf(editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED);
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
boolean syncRegistrations = Boolean.valueOf(model.getConfig().getFirst(LDAPConstants.SYNC_REGISTRATIONS));
String alwaysReadValueFromLDAP = String.valueOf(editMode== UserStorageProvider.EditMode.READ_ONLY || editMode== UserStorageProvider.EditMode.WRITABLE);
@ -420,6 +426,15 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
}
// In case that "Sync Registration" is ON and the LDAP v3 Password-modify extension is ON, we will create hardcoded mapper to create
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
if (!activeDirectory && syncRegistrations && ldapConfig.useExtendedPasswordModifyOp()) {
mapperModel = KeycloakModelUtils.createComponentModel("random initial password", model.getId(), HardcodedLDAPAttributeMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_NAME, LDAPConstants.USER_PASSWORD_ATTRIBUTE,
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_VALUE, HardcodedLDAPAttributeMapper.RANDOM_ATTRIBUTE_VALUE);
realm.addComponentModel(mapperModel);
}
}
@Override

View file

@ -17,8 +17,11 @@
package org.keycloak.storage.ldap.idm.store;
import java.util.Set;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
@ -93,6 +96,17 @@ public interface IdentityStore {
//
// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);
/**
* Query the LDAP server <a href="https://ldapwiki.com/wiki/RootDSE">RootDSE</a> and extract the {@link LDAPCapabilityRepresentation}
* of all supported <i>extensions</i>, <i>controls</i> and <i>features</i> the server announces. The LDAP Wiki
* provides a <a href="https://ldapwiki.com/wiki/LDAP%20Extensions%20and%20Controls%20Listing">list of known capabilities</a>.
*
* Will throw a {@link ModelException} on any LDAP error, or when the searchResult is empty.
*
* @return a set of LDAPOid, each representing a server capability (control, extension or feature).
*/
Set<LDAPCapabilityRepresentation> queryServerCapabilities();
// Credentials
/**

View file

@ -23,8 +23,10 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation.CapabilityType;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
@ -306,6 +308,40 @@ public class LDAPIdentityStore implements IdentityStore {
return resultCount;
}
@Override
public Set<LDAPCapabilityRepresentation> queryServerCapabilities() {
Set<LDAPCapabilityRepresentation> result = new LinkedHashSet<>();
try {
List<String> attrs = new ArrayList<>();
attrs.add("supportedControl");
attrs.add("supportedExtension");
attrs.add("supportedFeatures");
List<SearchResult> searchResults = operationManager
.search("", "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
if (searchResults.size() != 1) {
throw new ModelException("Could not query root DSE: unexpected result size");
}
SearchResult rootDse = searchResults.get(0);
Attributes attributes = rootDse.getAttributes();
for (String attr: attrs) {
Attribute attribute = attributes.get(attr);
if (null != attribute) {
CapabilityType capabilityType = CapabilityType.fromRootDseAttributeName(attr);
NamingEnumeration<?> values = attribute.getAll();
while (values.hasMoreElements()) {
Object o = values.nextElement();
LDAPCapabilityRepresentation capability = new LDAPCapabilityRepresentation(o, capabilityType);
logger.info("rootDSE query: " + capability);
result.add(capability);
}
}
}
return result;
} catch (NamingException e) {
throw new ModelException("Failed to query root DSE: " + e.getMessage(), e);
}
}
// *************** CREDENTIALS AND USER SPECIFIC STUFF
@Override
@ -329,24 +365,25 @@ public class LDAPIdentityStore implements IdentityStore {
if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
return;
}
try {
try {
if (config.useExtendedPasswordModifyOp()) {
operationManager.passwordModifyExtended(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
}
private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
try {
// Replace the "unicdodePwd" attribute with a new value

View file

@ -25,6 +25,7 @@ import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
import javax.naming.AuthenticationException;
@ -669,6 +670,25 @@ public class LDAPOperationManager {
return entryUUID.toString();
}
/**
* Execute the LDAP Password Modify Extended Operation to update the password for the given DN.
*
* @param dn distinguished name of the entry.
* @param password the new password.
* @param decorator A decorator to apply to the ldap operation.
*/
public void passwordModifyExtended(String dn, String password, LDAPOperationDecorator decorator) {
try {
execute(context -> {
PasswordModifyRequest modifyRequest = new PasswordModifyRequest(dn, null, password);
return context.extendedOperation(modifyRequest);
}, decorator);
} catch (NamingException e) {
throw new ModelException("Could not execute the password modify extended operation for DN [" + dn + "]", e);
}
}
private <R> R execute(LdapOperation<R> operation) throws NamingException {
return execute(operation, null);
}

View file

@ -0,0 +1,118 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* 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
*
* https://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.idm.store.ldap.extended;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import javax.naming.ldap.ExtendedRequest;
import javax.naming.ldap.ExtendedResponse;
/**
* An implementation of the
* <a target="_blank" href="https://tools.ietf.org/html/rfc3062">
* LDAP Password Modify Extended Operation
* </a>
* client request.
* <p>
* Can be directed at any LDAP server that supports the Password Modify Extended Operation.
*
* @author Josh Cummings
* @since 4.2.9
*/
public final class PasswordModifyRequest implements ExtendedRequest {
public static final String PASSWORD_MODIFY_OID = "1.3.6.1.4.1.4203.1.11.1";
private static final byte SEQUENCE_TYPE = 48;
private static final byte USER_IDENTITY_OCTET_TYPE = -128;
private static final byte OLD_PASSWORD_OCTET_TYPE = -127;
private static final byte NEW_PASSWORD_OCTET_TYPE = -126;
private final ByteArrayOutputStream value = new ByteArrayOutputStream();
public PasswordModifyRequest(String userIdentity, String oldPassword, String newPassword) {
ByteArrayOutputStream elements = new ByteArrayOutputStream();
if (userIdentity != null) {
berEncode(USER_IDENTITY_OCTET_TYPE, userIdentity.getBytes(), elements);
}
if (oldPassword != null) {
berEncode(OLD_PASSWORD_OCTET_TYPE, oldPassword.getBytes(), elements);
}
if (newPassword != null) {
berEncode(NEW_PASSWORD_OCTET_TYPE, newPassword.getBytes(), elements);
}
berEncode(SEQUENCE_TYPE, elements.toByteArray(), this.value);
}
@Override
public String getID() {
return PASSWORD_MODIFY_OID;
}
@Override
public byte[] getEncodedValue() {
return this.value.toByteArray();
}
@Override
public ExtendedResponse createExtendedResponse(String id, byte[] berValue, int offset, int length) {
return null;
}
/**
* Only minimal support for
* <a target="_blank" href="https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf">
* BER encoding
* </a>; just what is necessary for the Password Modify request.
*/
private void berEncode(byte type, byte[] src, ByteArrayOutputStream dest) {
int length = src.length;
dest.write(type);
if (length < 128) {
dest.write(length);
} else if ((length & 0x0000_00FF) == length) {
dest.write((byte) 0x81);
dest.write((byte) (length & 0xFF));
} else if ((length & 0x0000_FFFF) == length) {
dest.write((byte) 0x82);
dest.write((byte) ((length >> 8) & 0xFF));
dest.write((byte) (length & 0xFF));
} else if ((length & 0x00FF_FFFF) == length) {
dest.write((byte) 0x83);
dest.write((byte) ((length >> 16) & 0xFF));
dest.write((byte) ((length >> 8) & 0xFF));
dest.write((byte) (length & 0xFF));
} else {
dest.write((byte) 0x84);
dest.write((byte) ((length >> 24) & 0xFF));
dest.write((byte) ((length >> 16) & 0xFF));
dest.write((byte) ((length >> 8) & 0xFF));
dest.write((byte) (length & 0xFF));
}
try {
dest.write(src);
} catch (IOException e) {
throw new IllegalArgumentException("Failed to BER encode provided value of type: " + type);
}
}
}

View file

@ -41,6 +41,13 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
public static final String LDAP_ATTRIBUTE_VALUE = "ldap.attribute.value";
private static final String RANDOM = "RANDOM";
/**
* When this is configured as LDAP_ATTRIBUTE_VALUE, the mapper will use randomly generated value
*/
public static final String RANDOM_ATTRIBUTE_VALUE = "${" + RANDOM + "}";
public static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
public HardcodedLDAPAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
@ -65,7 +72,7 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
while (m.find()) {
String token = m.group(1);
if (token.equals("RANDOM")) {
if (token.equals(RANDOM)) {
String randomVal = getRandomValue();
m.appendReplacement(sb, randomVal);
} else {

View file

@ -0,0 +1,56 @@
package org.keycloak.storage.ldap.idm.model;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.LinkedHashSet;
import java.util.Set;
import org.junit.Test;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation.CapabilityType;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
public class LDAPCapabilityTest {
@Test
public void testEquals() {
LDAPCapabilityRepresentation oid1 = new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, CapabilityType.CONTROL);
LDAPCapabilityRepresentation oid2 = new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, CapabilityType.EXTENSION);
LDAPCapabilityRepresentation oid3 = new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, CapabilityType.EXTENSION);
assertFalse(oid1.equals(oid2));
assertTrue(oid2.equals(oid3));
System.out.println(oid1);
}
@Test
public void testContains() {
LDAPCapabilityRepresentation oid1 = new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, CapabilityType.EXTENSION);
LDAPCapabilityRepresentation oidx = new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, CapabilityType.EXTENSION);
LDAPCapabilityRepresentation oid2 = new LDAPCapabilityRepresentation("13.2.3.11.22", CapabilityType.CONTROL);
LDAPCapabilityRepresentation oid3 = new LDAPCapabilityRepresentation("14.2.3.42.22", CapabilityType.FEATURE);
Set<LDAPCapabilityRepresentation> ids = new LinkedHashSet<>();
ids.add(oid1);
ids.add(oidx);
ids.add(oid2);
ids.add(oid3);
assertTrue(ids.contains(oid1));
assertTrue(ids.contains(oidx));
assertEquals(3, ids.size());
}
@Test
public void testCapabilityTypeFromAttributeName() {
CapabilityType extension = CapabilityType.fromRootDseAttributeName("supportedExtension");
assertEquals(CapabilityType.EXTENSION, extension);
CapabilityType control = CapabilityType.fromRootDseAttributeName("supportedControl");
assertEquals(CapabilityType.CONTROL, control);
CapabilityType feature = CapabilityType.fromRootDseAttributeName("supportedFeatures");
assertEquals(CapabilityType.FEATURE, feature);
CapabilityType unknown = CapabilityType.fromRootDseAttributeName("foo");
assertEquals(CapabilityType.UNKNOWN, unknown);
}
}

View file

@ -24,6 +24,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -222,6 +223,13 @@ public interface RealmResource {
@NoCache
Response testLDAPConnection(TestLdapConnectionRepresentation config);
@POST
@Path("ldap-server-capabilities")
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
List<LDAPCapabilityRepresentation> ldapServerCapabilities(TestLdapConnectionRepresentation config);
@Path("testSMTPConnection")
@POST
@NoCache

View file

@ -35,6 +35,9 @@ public class LDAPConstants {
public static final String VENDOR_TIVOLI = "tivoli";
public static final String VENDOR_NOVELL_EDIRECTORY="edirectory" ;
// Could be discovered by rootDse supportedExtension: 1.3.6.1.4.1.4203.1.11.1
public static final String USE_PASSWORD_MODIFY_EXTENDED_OP = "usePasswordModifyExtendedOp";
public static final String USERNAME_LDAP_ATTRIBUTE = "usernameLDAPAttribute";
public static final String RDN_LDAP_ATTRIBUTE = "rdnLDAPAttribute";
public static final String UUID_LDAP_ATTRIBUTE = "uuidLDAPAttribute";
@ -66,6 +69,7 @@ public class LDAPConstants {
public static final String CONNECTION_POOLING_TIMEOUT = "connectionPoolingTimeout";
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
public static final String READ_TIMEOUT = "readTimeout";
// Could be discovered by rootDse supportedControl: 1.2.840.113556.1.4.319
public static final String PAGINATION = "pagination";
public static final String EDIT_MODE = "editMode";
@ -84,6 +88,7 @@ public class LDAPConstants {
// Custom user search filter
public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter";
// Could be discovered by rootDse supportedExtension: 1.3.6.1.4.1.1466.20037
public static final String START_TLS = "startTls";
// Custom attributes on UserModel, which is mapped to LDAP

View file

@ -1,86 +0,0 @@
/*
* 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.services.managers;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.services.ServicesLogger;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPContextManager;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPConnectionTestManager {
private static final Logger logger = Logger.getLogger(LDAPConnectionTestManager.class);
public static final String TEST_CONNECTION = "testConnection";
public static final String TEST_AUTHENTICATION = "testAuthentication";
public static boolean testLDAP(KeycloakSession session, String action, String connectionUrl, String bindDn,
String bindCredential, String useTruststoreSpi, String connectionTimeout, String tls) {
if (!TEST_CONNECTION.equals(action) && !TEST_AUTHENTICATION.equals(action)) {
ServicesLogger.LOGGER.unknownAction(action);
return false;
}
// Prepare MultivaluedHashMap so that it is usable in LDAPContext class
MultivaluedHashMap<String, String> ldapConfig = new MultivaluedHashMap<>();
if (connectionUrl == null) {
logger.errorf("Unknown connection URL");
return false;
}
ldapConfig.putSingle(LDAPConstants.CONNECTION_URL, connectionUrl);
ldapConfig.putSingle(LDAPConstants.USE_TRUSTSTORE_SPI, useTruststoreSpi);
ldapConfig.putSingle(LDAPConstants.CONNECTION_TIMEOUT, connectionTimeout);
ldapConfig.putSingle(LDAPConstants.START_TLS, tls);
if (TEST_AUTHENTICATION.equals(action)) {
// If AUTHENTICATION action is executed add also dn and credentials to configuration
// LDAPContextManager is responsible for correct order of addition of credentials to context in case
// tls is true
if (bindDn == null) {
logger.error("Unknown bind DN");
return false;
}
ldapConfig.putSingle(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE_SIMPLE);
ldapConfig.putSingle(LDAPConstants.BIND_DN, bindDn);
ldapConfig.putSingle(LDAPConstants.BIND_CREDENTIAL, bindCredential);
} else {
ldapConfig.putSingle(LDAPConstants.AUTH_TYPE, LDAPConstants.AUTH_TYPE_NONE);
}
// Create ldapContextManager in try-with-resource so that ldapContext/tlsResponse/VaultSecret is closed/removed when it is not needed anymore
try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, new LDAPConfig(ldapConfig))) {
ldapContextManager.getLdapContext();
// Connection was successful, no exception was raised returning true
return true;
} catch (Exception ne) {
String errorMessage = (TEST_AUTHENTICATION.equals(action)) ? "Error when authenticating to LDAP: " : "Error when connecting to LDAP: ";
ServicesLogger.LOGGER.errorAuthenticating(ne, errorMessage + ne.getMessage());
return false;
}
}
}

View file

@ -0,0 +1,110 @@
/*
* 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.services.managers;
import java.util.Collections;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
import org.keycloak.services.ServicesLogger;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPContextManager;
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LDAPServerCapabilitiesManager {
private static final Logger logger = Logger.getLogger(LDAPServerCapabilitiesManager.class);
public static final String TEST_CONNECTION = "testConnection";
public static final String TEST_AUTHENTICATION = "testAuthentication";
public static final String QUERY_SERVER_CAPABILITIES = "queryServerCapabilities";
public static LDAPConfig buildLDAPConfig(TestLdapConnectionRepresentation config, RealmModel realm) {
String bindCredential = config.getBindCredential();
if (config.getComponentId() != null && ComponentRepresentation.SECRET_VALUE.equals(bindCredential)) {
bindCredential = realm.getComponent(config.getComponentId()).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL);
}
MultivaluedHashMap<String, String> configMap = new MultivaluedHashMap<>();
configMap.putSingle(LDAPConstants.AUTH_TYPE, config.getAuthType());
configMap.putSingle(LDAPConstants.BIND_DN, config.getBindDn());
configMap.putSingle(LDAPConstants.BIND_CREDENTIAL, bindCredential);
configMap.add(LDAPConstants.CONNECTION_URL, config.getConnectionUrl());
configMap.add(LDAPConstants.USE_TRUSTSTORE_SPI, config.getUseTruststoreSpi());
configMap.putSingle(LDAPConstants.CONNECTION_TIMEOUT, config.getConnectionTimeout());
configMap.add(LDAPConstants.START_TLS, config.getStartTls());
return new LDAPConfig(configMap);
}
public static Set<LDAPCapabilityRepresentation> queryServerCapabilities(TestLdapConnectionRepresentation config, KeycloakSession session,
RealmModel realm) {
if (! QUERY_SERVER_CAPABILITIES.equals(config.getAction())) {
ServicesLogger.LOGGER.unknownAction(config.getAction());
return Collections.emptySet();
}
LDAPConfig ldapConfig = buildLDAPConfig(config, realm);
return new LDAPIdentityStore(session, ldapConfig).queryServerCapabilities();
}
public static boolean testLDAP(TestLdapConnectionRepresentation config, KeycloakSession session, RealmModel realm) {
if (!TEST_CONNECTION.equals(config.getAction()) && !TEST_AUTHENTICATION.equals(config.getAction())) {
ServicesLogger.LOGGER.unknownAction(config.getAction());
return false;
}
if (TEST_AUTHENTICATION.equals(config.getAction())) {
// If AUTHENTICATION action is executed add also dn and credentials to configuration
// LDAPContextManager is responsible for correct order of addition of credentials to context in case
// tls is true
if (config.getBindDn() == null || config.getBindDn().isEmpty()) {
logger.error("Unknown bind DN");
return false;
}
} else {
// only test the connection.
config.setAuthType(LDAPConstants.AUTH_TYPE_NONE);
}
LDAPConfig ldapConfig = buildLDAPConfig(config, realm);
// Create ldapContextManager in try-with-resource so that ldapContext/tlsResponse/VaultSecret is closed/removed when it
// is not needed anymore
try (LDAPContextManager ldapContextManager = LDAPContextManager.create(session, ldapConfig)) {
ldapContextManager.getLdapContext();
// Connection was successful, no exception was raised returning true
return true;
} catch (Exception ne) {
String errorMessage = (TEST_AUTHENTICATION.equals(config.getAction())) ? "Error when authenticating to LDAP: "
: "Error when connecting to LDAP: ";
ServicesLogger.LOGGER.errorAuthenticating(ne, errorMessage + ne.getMessage());
return false;
}
}
}

View file

@ -16,21 +16,49 @@
*/
package org.keycloak.services.resources.admin;
import com.fasterxml.jackson.core.type.TypeReference;
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
import static org.keycloak.util.JsonSerialization.readValue;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.Config;
import org.keycloak.KeyPairVerifier;
import org.keycloak.authentication.CredentialRegistrator;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventQuery;
@ -45,7 +73,18 @@ import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.exportimport.util.ExportOptions;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.keys.PublicKeyStorageProvider;
import org.keycloak.models.*;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.KeycloakModelUtils;
@ -69,7 +108,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.LDAPServerCapabilitiesManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.UserStorageSyncManager;
@ -77,37 +116,11 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.storage.UserStorageProviderModel;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
import static org.keycloak.util.JsonSerialization.readValue;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.utils.ReservedCharValidator;
import com.fasterxml.jackson.core.type.TypeReference;
/**
* Base resource class for the admin REST api of one realm
*
@ -938,11 +951,9 @@ public class RealmAdminResource {
@FormParam("componentId") String componentId, @FormParam("startTls") String startTls) {
auth.realm().requireManageRealm();
if (componentId != null && bindCredential.equals(ComponentRepresentation.SECRET_VALUE)) {
bindCredential = realm.getComponent(componentId).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL);
}
boolean result = LDAPConnectionTestManager.testLDAP(session, action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls);
TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls, LDAPConstants.AUTH_TYPE_SIMPLE);
config.setComponentId(componentId);
boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
@ -955,15 +966,28 @@ public class RealmAdminResource {
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
public Response testLDAPConnection(TestLdapConnectionRepresentation config) {
return testLDAPConnection(
config.getAction(),
config.getConnectionUrl(),
config.getBindDn(),
config.getBindCredential(),
config.getUseTruststoreSpi(),
config.getConnectionTimeout(),
config.getComponentId(),
config.getStartTls());
boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
/**
* Get LDAP supported extensions.
* @param config LDAP configuration
* @return
*/
@POST
@Path("ldap-server-capabilities")
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
public Response ldapServerCapabilities(TestLdapConnectionRepresentation config) {
auth.realm().requireManageRealm();
try {
Set<LDAPCapabilityRepresentation> ldapCapabilities = LDAPServerCapabilitiesManager.queryServerCapabilities(config, session, realm);
return Response.ok().entity(ldapCapabilities).build();
} catch (Exception e) {
return ErrorResponse.error("ldapServerCapabilities error", Status.BAD_REQUEST);
}
}
/**

View file

@ -616,6 +616,7 @@ public class UserResource {
} catch (ReadOnlyException mre) {
throw new BadRequestException("Can't reset password as account is read only");
} catch (ModelException e) {
logger.warn("Could not update user password.", e);
Properties messages = AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale());
throw new ErrorResponseException(e.getMessage(), MessageFormat.format(messages.getProperty(e.getMessage(), e.getMessage()), e.getParameters()),
Status.BAD_REQUEST);

View file

@ -17,14 +17,21 @@
package org.keycloak.testsuite.admin;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.ClassRule;
import org.junit.Test;
import org.keycloak.models.LDAPConstants;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
import org.keycloak.services.managers.LDAPConnectionTestManager;
import org.keycloak.services.managers.LDAPServerCapabilitiesManager;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.util.LDAPRule;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@ -46,30 +53,30 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
assertStatus(response, 400);
// Bad host
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false", null));
assertStatus(response, 400);
// Connection success
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false", null, "false", LDAPConstants.AUTH_TYPE_NONE));
assertStatus(response, 204);
// Bad authentication
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false", "10000"));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false", "10000"));
assertStatus(response, 400);
// Authentication success
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false", null));
assertStatus(response, 204);
// Authentication success with bindCredential from Vault
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
assertStatus(response, 204);
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
assertStatus(response, 204);
// Deprecated form based
response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null);
response = realm.testLDAPConnection(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null);
assertStatus(response, 204);
}
@ -77,26 +84,50 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
@Test
public void testLdapConnectionsSsl() {
Response response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false", null));
Response response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false", null, null, LDAPConstants.AUTH_TYPE_NONE));
assertStatus(response, 204);
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false", null));
assertStatus(response, 400);
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false", null));
assertStatus(response, 400);
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 204);
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000"));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000"));
assertStatus(response, 204);
// Authentication success with bindCredential from Vault
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "true", null));
response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "true", null));
assertStatus(response, 204);
}
@Test
public void testLdapCapabilities() {
// Query the rootDSE success
TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation(
LDAPServerCapabilitiesManager.QUERY_SERVER_CAPABILITIES, "ldap://localhost:10389", "uid=admin,ou=system", "secret",
"false", null, "false", LDAPConstants.AUTH_TYPE_SIMPLE);
List<LDAPCapabilityRepresentation> ldapCapabilities = realm.ldapServerCapabilities(config);
Assert.assertThat(ldapCapabilities, Matchers.hasItem(new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, LDAPCapabilityRepresentation.CapabilityType.EXTENSION)));
// Query the rootDSE failure
try {
config = new TestLdapConnectionRepresentation(
LDAPServerCapabilitiesManager.QUERY_SERVER_CAPABILITIES, "ldap://localhost:10389", "foo", "bar",
"false", null, "false", LDAPConstants.AUTH_TYPE_SIMPLE);
realm.ldapServerCapabilities(config);
Assert.fail("It wasn't expected to successfully sent the request for query capabilities");
} catch (BadRequestException bre) {
// Expected
}
}
private void assertStatus(Response response, int status) {
Assert.assertEquals(status, response.getStatus());
response.close();

View file

@ -0,0 +1,146 @@
/*
* Copyright 2019 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 org.junit.Assert;
import org.junit.ClassRule;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProviderModel;
import org.keycloak.storage.ldap.LDAPStorageProvider;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapper;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.util.LDAPRule;
import org.keycloak.testsuite.util.LDAPTestConfiguration;
import org.keycloak.testsuite.util.LDAPTestUtils;
/**
* Test for the LDAPv3 Password modify extension (https://tools.ietf.org/html/rfc3062)
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class LDAPPasswordModifyExtensionTest extends AbstractLDAPTest {
// Run this test for embedded ApacheDS
@ClassRule
public static LDAPRule ldapRule = new LDAPRule()
.assumeTrue((LDAPTestConfiguration ldapConfig) -> {
return (ldapConfig.isStartEmbeddedLdapServer());
});
@Override
protected LDAPRule getLDAPRule() {
return ldapRule;
}
@Override
protected void afterImportTestRealm() {
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
// Enable Password Modify extension
UserStorageProviderModel model = ctx.getLdapModel();
model.put(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP, true);
appRealm.updateComponent(model);
ComponentModel randomLDAPPasswordMapper = KeycloakModelUtils.createComponentModel("random initial password", model.getId(), HardcodedLDAPAttributeMapperFactory.PROVIDER_ID, LDAPStorageMapper.class.getName(),
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_NAME, LDAPConstants.USER_PASSWORD_ATTRIBUTE,
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_VALUE, HardcodedLDAPAttributeMapper.RANDOM_ATTRIBUTE_VALUE);
appRealm.addComponentModel(randomLDAPPasswordMapper);
});
testingClient.server().run(session -> {
LDAPTestContext ctx = LDAPTestContext.init(session);
RealmModel appRealm = ctx.getRealm();
// Delete all LDAP users and add some new for testing
LDAPStorageProvider ldapFedProvider = LDAPTestUtils.getLdapProvider(session, ctx.getLdapModel());
LDAPTestUtils.removeAllLDAPUsers(ldapFedProvider, appRealm);
LDAPObject john = LDAPTestUtils.addLDAPUser(ldapFedProvider, appRealm, "johnkeycloak", "John", "Doe", "john@email.org", null, "1234");
LDAPTestUtils.updateLDAPPassword(ldapFedProvider, john, "Password1");
appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
});
}
@Test
public void ldapPasswordChangeWithAccountConsole() throws Exception {
changePasswordPage.open();
loginPage.login("johnkeycloak", "Password1");
changePasswordPage.changePassword("Password1", "New-password1", "New-password1");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
changePasswordPage.logout();
loginPage.open();
loginPage.login("johnkeycloak", "Bad-password1");
Assert.assertEquals("Invalid username or password.", loginPage.getError());
loginPage.open();
loginPage.login("johnkeycloak", "New-password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
// Change password back to previous value
changePasswordPage.open();
changePasswordPage.changePassword("New-password1", "Password1", "Password1");
Assert.assertEquals("Your password has been updated.", profilePage.getSuccess());
}
@Test
public void registerUserLdapSuccess() {
loginPage.open();
loginPage.clickRegister();
registerPage.assertCurrent();
registerPage.register("firstName", "lastName", "email2@check.cz", "registerUserSuccess2", "Password1", "Password1");
Assert.assertEquals(AppPage.RequestType.AUTH_RESPONSE, appPage.getRequestType());
UserRepresentation user = ApiUtil.findUserByUsername(testRealm(),"registerUserSuccess2");
Assert.assertNotNull(user);
assertFederatedUserLink(user);
Assert.assertEquals("registerusersuccess2", user.getUsername());
Assert.assertEquals("firstName", user.getFirstName());
Assert.assertEquals("lastName", user.getLastName());
Assert.assertTrue(user.isEnabled());
}
protected void assertFederatedUserLink(UserRepresentation user) {
Assert.assertTrue(StorageId.isLocalStorage(user.getId()));
Assert.assertNotNull(user.getFederationLink());
Assert.assertEquals(user.getFederationLink(), ldapModelId);
}
}

View file

@ -60,6 +60,9 @@ public class LdapUserProviderForm extends Form {
@FindBy(id = "searchScope")
private Select searchScopeSelect;
@FindBy(id = "kerberosIntegrationHeader")
private WebElement kerberosIntegrationHeader;
@FindBy(id = "kerberosRealm")
private WebElement kerberosRealmInput;
@ -123,7 +126,7 @@ public class LdapUserProviderForm extends Form {
@FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='changedSyncEnabled']]")
private OnOffSwitch periodicChangedUsersSync;
@FindByJQuery("a:contains('Connection Pooling Settings')")
@FindBy(id = "connectionPoolSettingsHeader")
private WebElement connectionPoolingSettingsButton;
@FindBy(id = "connectionPoolingAuthentication")
@ -191,6 +194,16 @@ public class LdapUserProviderForm extends Form {
UIUtils.setTextInputValue(customUserSearchFilterInput, customUserSearchFilter);
}
public void uncollapseKerberosIntegrationHeader() {
if (UIUtils.isElementVisible(kerberosRealmInput)) {
// Already collapsed
return;
}
kerberosIntegrationHeader.click();
waitUntilElement(By.id("kerberosRealm")).is().present();
}
public void setKerberosRealmInput(String kerberosRealm) {
UIUtils.setTextInputValue(kerberosRealmInput, kerberosRealm);
}

View file

@ -45,6 +45,7 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
createLdapUserProvider.form().setLdapBindCredentialInput("secret");
// createLdapUserProvider.form().setAccountAfterPasswordUpdateEnabled(false);
// enable kerberos
createLdapUserProvider.form().uncollapseKerberosIntegrationHeader();
createLdapUserProvider.form().setAllowKerberosAuthEnabled(true);
createLdapUserProvider.form().setKerberosRealmInput("KEYCLOAK.ORG");
createLdapUserProvider.form().setServerPrincipalInput("HTTP/localhost@KEYCLOAK.ORG");

View file

@ -945,6 +945,8 @@ import-enabled=Import Users
ldap.import-enabled.tooltip=If true, LDAP users will be imported into Keycloak DB and synced by the configured sync policies.
vendor=Vendor
ldap.vendor.tooltip=LDAP vendor (provider)
enable-usePasswordModifyExtendedOp=Enable the LDAPv3 Password Modify Extended Operation
ldap.usePasswordModifyExtendedOp.tooltip=Use the LDAPv3 Password Modify Extended Operation (RFC-3062). The password modify extended operation usually requires that LDAP user already has password in the LDAP server. So when this is used with 'Sync Registrations', it can be good to add also 'Hardcoded LDAP attribute mapper' with randomly generated initial password.
username-ldap-attribute=Username LDAP attribute
ldap-attribute-name-for-username=LDAP attribute name for username
username-ldap-attribute.tooltip=Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server vendors it can be 'uid'. For Active directory it can be 'sAMAccountName' or 'cn'. The attribute should be filled for all LDAP user records you want to import from LDAP to Keycloak.
@ -1623,3 +1625,8 @@ pkce-code-challenge-method=Proof Key for Code Exchange Code Challenge Method
pkce-code-challenge-method.tooltip=Choose which code challenge method for PKCE is used. If not specified, keycloak does not applies PKCE to a client unless the client sends an authorization request with appropriate code challenge and code exchange method.
key-not-allowed-here=Key '{{character}}' is not allowed here.
# KEYCLOAK-10927 Implement LDAPv3 Password Modify Extended Operation
advanced-ldap-settings=Advanced Settings
ldap-query-supported-extensions=Query Supported Extensions
ldap-query-supported-extensions.tooltip=This will query LDAP server for supported extensions, controls and features. Some advanced settings of the LDAP provider will be then automatically configured based on the capabilities/extensions/features supported by LDAP server. For example if LDAPv3 Password Modify extension is supported by LDAP server, corresponding switch will be enabled for LDAP provider.

View file

@ -1364,7 +1364,8 @@ module.controller('UserGroupMembershipCtrl', function($scope, $q, realm, user, U
});
module.controller('LDAPUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
serverInfo, instance, Components, UserStorageOperations, RealmLDAPConnectionTester) {
serverInfo, instance, Components, UserStorageOperations,
RealmLDAPConnectionTester, $http) {
console.log('LDAPUserStorageCtrl');
var providerId = 'ldap';
console.log('providerId: ' + providerId);
@ -1672,10 +1673,12 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
Notifications.error("Error during unlink");
});
};
var initConnectionTest = function(testAction, ldapConfig) {
return {
action: testAction,
connectionUrl: ldapConfig.connectionUrl && ldapConfig.connectionUrl[0],
authType: ldapConfig.authType && ldapConfig.authType[0],
bindDn: ldapConfig.bindDn && ldapConfig.bindDn[0],
bindCredential: ldapConfig.bindCredential && ldapConfig.bindCredential[0],
useTruststoreSpi: ldapConfig.useTruststoreSpi && ldapConfig.useTruststoreSpi[0],
@ -1703,7 +1706,23 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
});
}
$scope.queryAndSetLdapSupportedExtensions = function() {
console.log('LDAPCtrl: getLdapSupportedExtensions');
const PASSWORD_MODIFY_OID = '1.3.6.1.4.1.4203.1.11.1';
$http.post(
`${authUrl}/admin/realms/${realm.realm}/ldap-server-capabilities`,
initConnectionTest("queryServerCapabilities", $scope.instance.config)).then(
(response) => {
Notifications.success("LDAP supported extensions successfully requested.");
const ldapOids = response.data;
if (angular.isArray(ldapOids)) {
const passwordModifyOid = ldapOids.filter(ldapOid => ldapOid.oid === PASSWORD_MODIFY_OID);
$scope.instance.config['usePasswordModifyExtendedOp'][0] = `${passwordModifyOid.length > 0}`;
}
},
() => Notifications.error("Error when trying to request supported extensions of LDAP. See server.log for details."));
}
});

View file

@ -127,43 +127,6 @@
</div>
<kc-tooltip>{{:: 'ldap.users-dn.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="authType"><span class="required">*</span> {{:: 'authentication-type' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="authType"
ng-model="instance.config['authType'][0]"
ng-options="authType.id as authType.name for authType in authTypes"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'ldap.authentication-type.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="startTls">{{:: 'enable-start-tls' | translate}}</label>
<div class="col-md-6">
<input ng-model="instance.config['startTls'][0]" name="startTls" id="startTls" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'ldap.startTls.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
<label class="col-md-2 control-label" for="ldapBindDn"><span class="required">*</span> {{:: 'bind-dn' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="ldapBindDn" type="text" ng-model="instance.config['bindDn'][0]" placeholder="{{:: 'ldap-bind-dn' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
</div>
<kc-tooltip>{{:: 'ldap.bind-dn.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
<label class="col-md-2 control-label" for="ldapBindCred"><span class="required">*</span> {{:: 'bind-credential' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="ldapBindCred" kc-password ng-model="instance.config['bindCredential'][0]" placeholder="{{:: 'ldap-bind-credentials' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
</div>
<kc-tooltip>{{:: 'ldap.bind-credential.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-4" data-ng-show="access.manageRealm">
<a class="btn btn-primary" data-ng-click="testAuthentication()">{{:: 'test-authentication' | translate}}</a>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="customUserSearchFilter">{{:: 'custom-user-ldap-filter' | translate}}</label>
<div class="col-md-6">
@ -184,6 +147,60 @@
</div>
<kc-tooltip>{{:: 'ldap.search-scope.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="authType"><span class="required">*</span> {{:: 'authentication-type' | translate}}</label>
<div class="col-md-6">
<div>
<select class="form-control" id="authType"
ng-model="instance.config['authType'][0]"
ng-options="authType.id as authType.name for authType in authTypes"
required>
</select>
</div>
</div>
<kc-tooltip>{{:: 'ldap.authentication-type.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
<label class="col-md-2 control-label" for="ldapBindDn"><span class="required">*</span> {{:: 'bind-dn' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="ldapBindDn" type="text" ng-model="instance.config['bindDn'][0]" placeholder="{{:: 'ldap-bind-dn' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
</div>
<kc-tooltip>{{:: 'ldap.bind-dn.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-hide="instance.config['authType'][0] == 'none'">
<label class="col-md-2 control-label" for="ldapBindCred"><span class="required">*</span> {{:: 'bind-credential' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="ldapBindCred" kc-password ng-model="instance.config['bindCredential'][0]" placeholder="{{:: 'ldap-bind-credentials' | translate}}" data-ng-required="instance.config['authType'][0] != 'none'">
</div>
<kc-tooltip>{{:: 'ldap.bind-credential.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-4" data-ng-show="access.manageRealm">
<a class="btn btn-primary" data-ng-click="testAuthentication()">{{:: 'test-authentication' | translate}}</a>
</div>
</div>
</fieldset>
<fieldset>
<legend id="advancedLdapSettingsHeader" collapsed><span class="text">{{:: 'advanced-ldap-settings' | translate}}</span></legend>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="startTls">{{:: 'enable-start-tls' | translate}}</label>
<div class="col-md-6">
<input ng-model="instance.config['startTls'][0]" name="startTls" id="startTls" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'ldap.startTls.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="usePasswordModifyExtendedOp">{{:: 'enable-usePasswordModifyExtendedOp' | translate}}</label>
<div class="col-md-6">
<input ng-model="instance.config['usePasswordModifyExtendedOp'][0]" name="usePasswordModifyExtendedOp" id="usePasswordModifyExtendedOp" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'ldap.usePasswordModifyExtendedOp.tooltip' | translate}}</kc-tooltip>
<div>
<div class="col-sm-4" data-ng-show="access.manageRealm">
<a class="btn btn-primary" data-ng-click="queryAndSetLdapSupportedExtensions()">{{:: 'ldap-query-supported-extensions' | translate}}</a>
</div>
<kc-tooltip>{{:: 'ldap-query-supported-extensions.tooltip' | translate}}</kc-tooltip>
</div>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="validatePasswordPolicy">{{:: 'validate-password-policy' | translate}}</label>
<div class="col-md-6">
@ -211,65 +228,6 @@
</div>
<kc-tooltip>{{:: 'ldap.use-truststore-spi.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="connectionPooling">{{:: 'connection-pooling' | translate}}</label>
<div class="col-md-6">
<input ng-model="instance.config['connectionPooling'][0]" name="connectionPooling" id="connectionPooling" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.tooltip' | translate}}</kc-tooltip>
<div class="col-sm-4" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<a class="btn btn-primary" data-ng-init="connectionPoolSettings=false" data-ng-click="connectionPoolSettings=!connectionPoolSettings">{{:: 'connection-pooling-settings' | translate}}</a>
</div>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingAuthentication">{{:: 'connection-pooling-authentication' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingAuthentication" type="text" ng-model="instance.config['connectionPoolingAuthentication'][0]" placeholder="{{:: 'connection-pooling-authentication-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.authentication.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingDebug">{{:: 'connection-pooling-debug' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingDebug" type="text" ng-model="instance.config['connectionPoolingDebug'][0]" placeholder="{{:: 'connection-pooling-debug-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.debug.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingInitSize">{{:: 'connection-pooling-initsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingInitSize" type="text" ng-model="instance.config['connectionPoolingInitSize'][0]" placeholder="{{:: 'connection-pooling-initsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.initsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingMaxSize">{{:: 'connection-pooling-maxsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingMaxSize" type="text" ng-model="instance.config['connectionPoolingMaxSize'][0]" placeholder="{{:: 'connection-pooling-maxsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.maxsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingPrefSize">{{:: 'connection-pooling-prefsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingPrefSize" type="text" ng-model="instance.config['connectionPoolingPrefSize'][0]" placeholder="{{:: 'connection-pooling-prefsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.prefsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingProtocol">{{:: 'connection-pooling-protocol' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingProtocol" type="text" ng-model="instance.config['connectionPoolingProtocol'][0]" placeholder="{{:: 'connection-pooling-protocol-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.protocol.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true' && connectionPoolSettings">
<label class="col-md-2 control-label" for="connectionPoolingTimeout">{{:: 'connection-pooling-timeout' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingTimeout" type="text" ng-model="instance.config['connectionPoolingTimeout'][0]" placeholder="{{:: 'connection-pooling-timeout-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.timeout.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="connectionTimeout">{{:: 'ldap-connection-timeout' | translate}}</label>
<div class="col-md-6">
@ -294,7 +252,67 @@
</fieldset>
<fieldset>
<legend><span class="text">{{:: 'kerberos-integration' | translate}}</span></legend>
<legend id="connectionPoolSettingsHeader" collapsed><span class="text">{{:: 'connection-pooling' | translate}}</span></legend>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="connectionPooling">{{:: 'connection-pooling' | translate}}</label>
<div class="col-md-6">
<input ng-model="instance.config['connectionPooling'][0]" name="connectionPooling" id="connectionPooling" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingAuthentication">{{:: 'connection-pooling-authentication' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingAuthentication" type="text" ng-model="instance.config['connectionPoolingAuthentication'][0]" placeholder="{{:: 'connection-pooling-authentication-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.authentication.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingDebug">{{:: 'connection-pooling-debug' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingDebug" type="text" ng-model="instance.config['connectionPoolingDebug'][0]" placeholder="{{:: 'connection-pooling-debug-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.debug.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingInitSize">{{:: 'connection-pooling-initsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingInitSize" type="text" ng-model="instance.config['connectionPoolingInitSize'][0]" placeholder="{{:: 'connection-pooling-initsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.initsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingMaxSize">{{:: 'connection-pooling-maxsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingMaxSize" type="text" ng-model="instance.config['connectionPoolingMaxSize'][0]" placeholder="{{:: 'connection-pooling-maxsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.maxsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingPrefSize">{{:: 'connection-pooling-prefsize' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingPrefSize" type="text" ng-model="instance.config['connectionPoolingPrefSize'][0]" placeholder="{{:: 'connection-pooling-prefsize-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.prefsize.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingProtocol">{{:: 'connection-pooling-protocol' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingProtocol" type="text" ng-model="instance.config['connectionPoolingProtocol'][0]" placeholder="{{:: 'connection-pooling-protocol-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.protocol.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix" data-ng-show="instance.config['connectionPooling'][0] == 'true'">
<label class="col-md-2 control-label" for="connectionPoolingTimeout">{{:: 'connection-pooling-timeout' | translate}}</label>
<div class="col-md-6">
<input class="form-control" id="connectionPoolingTimeout" type="text" ng-model="instance.config['connectionPoolingTimeout'][0]" placeholder="{{:: 'connection-pooling-timeout-default' | translate}}"/>
</div>
<kc-tooltip>{{:: 'ldap.connection-pooling.timeout.tooltip' | translate}}</kc-tooltip>
</div>
</fieldset>
<fieldset>
<legend id="kerberosIntegrationHeader" collapsed><span class="text">{{:: 'kerberos-integration' | translate}}</span></legend>
<div class="form-group">
<label class="col-md-2 control-label" for="allowKerberosAuthentication">{{:: 'allow-kerberos-authentication' | translate}} </label>
<div class="col-md-6">
@ -340,7 +358,7 @@
</fieldset>
<fieldset>
<legend><span class="text">{{:: 'sync-settings' | translate}}</span></legend>
<legend id="syncSettingsHeader" collapsed><span class="text">{{:: 'sync-settings' | translate}}</span></legend>
<div class="form-group clearfix">
<label class="col-md-2 control-label" for="batchSizeForSync">{{:: 'batch-size' | translate}}</label>
<div class="col-md-6">
@ -379,7 +397,7 @@
</fieldset>
<fieldset>
<legend><span class="text">{{:: 'user-storage-cache-policy' | translate}}</span></legend>
<legend id="cachePolicyHeader" collapsed><span class="text">{{:: 'user-storage-cache-policy' | translate}}</span></legend>
<div class="form-group">
<label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
<div class="col-md-2">

View file

@ -22,6 +22,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.text.StrSubstitutor;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.exception.LdapEntryAlreadyExistsException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
@ -33,6 +34,7 @@ import org.apache.directory.server.core.factory.DefaultDirectoryServiceFactory;
import org.apache.directory.server.core.factory.JdbmPartitionFactory;
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
import org.apache.directory.server.ldap.LdapServer;
import org.apache.directory.server.ldap.handlers.extended.PwdModifyHandler;
import org.apache.directory.server.protocol.shared.transport.TcpTransport;
import org.apache.directory.server.protocol.shared.transport.Transport;
import org.jboss.logging.Logger;
@ -255,6 +257,13 @@ public class LDAPEmbeddedServer {
// Associate the DS to this LdapServer
ldapServer.setDirectoryService( directoryService );
// Support for extended password modify as described in https://tools.ietf.org/html/rfc3062
try {
ldapServer.addExtendedOperationHandler(new PwdModifyHandler());
} catch (LdapException le) {
throw new IllegalStateException("It wasn't possible to add PwdModifyHandler");
}
// Propagate the anonymous flag to the DS
directoryService.setAllowAnonymousAccess(false);