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:
parent
cc776204f0
commit
3382682115
24 changed files with 952 additions and 264 deletions
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,17 +10,24 @@ public class TestLdapConnectionRepresentation {
|
||||||
private String connectionTimeout;
|
private String connectionTimeout;
|
||||||
private String componentId;
|
private String componentId;
|
||||||
private String startTls;
|
private String startTls;
|
||||||
|
private String authType;
|
||||||
|
|
||||||
public TestLdapConnectionRepresentation() {
|
public TestLdapConnectionRepresentation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout) {
|
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.action = action;
|
||||||
this.connectionUrl = connectionUrl;
|
this.connectionUrl = connectionUrl;
|
||||||
this.bindDn = bindDn;
|
this.bindDn = bindDn;
|
||||||
this.bindCredential = bindCredential;
|
this.bindCredential = bindCredential;
|
||||||
this.useTruststoreSpi = useTruststoreSpi;
|
this.useTruststoreSpi = useTruststoreSpi;
|
||||||
this.connectionTimeout = connectionTimeout;
|
this.connectionTimeout = connectionTimeout;
|
||||||
|
this.startTls = startTls;
|
||||||
|
this.authType = authType;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAction() {
|
public String getAction() {
|
||||||
|
@ -39,6 +46,14 @@ public class TestLdapConnectionRepresentation {
|
||||||
this.connectionUrl = connectionUrl;
|
this.connectionUrl = connectionUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAuthType() {
|
||||||
|
return authType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAuthType(String authType) {
|
||||||
|
this.authType = authType;
|
||||||
|
}
|
||||||
|
|
||||||
public String getBindDn() {
|
public String getBindDn() {
|
||||||
return bindDn;
|
return bindDn;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
public String getUseTruststoreSpi() {
|
||||||
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
|
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.idm.store.ldap.LDAPIdentityStore;
|
||||||
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
|
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
|
||||||
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
|
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.LDAPConfigDecorator;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
|
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
|
||||||
|
@ -106,6 +108,9 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||||
.property().name(LDAPConstants.VENDOR)
|
.property().name(LDAPConstants.VENDOR)
|
||||||
.type(ProviderConfigProperty.STRING_TYPE)
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
.add()
|
.add()
|
||||||
|
.property().name(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP)
|
||||||
|
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||||
|
.add()
|
||||||
.property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
|
.property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
|
||||||
.type(ProviderConfigProperty.STRING_TYPE)
|
.type(ProviderConfigProperty.STRING_TYPE)
|
||||||
.add()
|
.add()
|
||||||
|
@ -308,6 +313,7 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
||||||
UserStorageProvider.EditMode editMode = ldapConfig.getEditMode();
|
UserStorageProvider.EditMode editMode = ldapConfig.getEditMode();
|
||||||
String readOnly = String.valueOf(editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED);
|
String readOnly = String.valueOf(editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED);
|
||||||
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
|
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);
|
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,
|
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
|
||||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
|
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
|
@Override
|
||||||
|
|
|
@ -17,8 +17,11 @@
|
||||||
|
|
||||||
package org.keycloak.storage.ldap.idm.store;
|
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.LDAPConfig;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
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.idm.query.internal.LDAPQuery;
|
||||||
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||||
|
|
||||||
|
@ -93,6 +96,17 @@ public interface IdentityStore {
|
||||||
//
|
//
|
||||||
// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);
|
// <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
|
// Credentials
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -23,8 +23,10 @@ import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.LDAPConstants;
|
import org.keycloak.models.LDAPConstants;
|
||||||
import org.keycloak.models.ModelException;
|
import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.storage.ldap.LDAPConfig;
|
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.LDAPDn;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPObject;
|
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.Condition;
|
||||||
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
|
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
|
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
|
||||||
|
@ -306,6 +308,40 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
return resultCount;
|
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
|
// *************** CREDENTIALS AND USER SPECIFIC STUFF
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -329,23 +365,24 @@ public class LDAPIdentityStore implements IdentityStore {
|
||||||
|
|
||||||
if (getConfig().isActiveDirectory()) {
|
if (getConfig().isActiveDirectory()) {
|
||||||
updateADPassword(userDN, password, passwordUpdateDecorator);
|
updateADPassword(userDN, password, passwordUpdateDecorator);
|
||||||
} else {
|
return;
|
||||||
ModificationItem[] mods = new ModificationItem[1];
|
}
|
||||||
|
|
||||||
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);
|
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);
|
||||||
|
|
||||||
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);
|
||||||
|
|
||||||
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
|
operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
|
||||||
|
}
|
||||||
} catch (ModelException me) {
|
} catch (ModelException me) {
|
||||||
throw me;
|
throw me;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new ModelException("Error updating password.", e);
|
throw new ModelException("Error updating password.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
|
private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.ModelException;
|
||||||
import org.keycloak.storage.ldap.LDAPConfig;
|
import org.keycloak.storage.ldap.LDAPConfig;
|
||||||
import org.keycloak.storage.ldap.idm.model.LDAPDn;
|
import org.keycloak.storage.ldap.idm.model.LDAPDn;
|
||||||
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
|
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 org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;
|
||||||
|
|
||||||
import javax.naming.AuthenticationException;
|
import javax.naming.AuthenticationException;
|
||||||
|
@ -669,6 +670,25 @@ public class LDAPOperationManager {
|
||||||
return entryUUID.toString();
|
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 {
|
private <R> R execute(LdapOperation<R> operation) throws NamingException {
|
||||||
return execute(operation, null);
|
return execute(operation, null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,13 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
public static final String LDAP_ATTRIBUTE_VALUE = "ldap.attribute.value";
|
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 static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
|
||||||
|
|
||||||
public HardcodedLDAPAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
public HardcodedLDAPAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
|
||||||
|
@ -65,7 +72,7 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
|
||||||
|
|
||||||
while (m.find()) {
|
while (m.find()) {
|
||||||
String token = m.group(1);
|
String token = m.group(1);
|
||||||
if (token.equals("RANDOM")) {
|
if (token.equals(RANDOM)) {
|
||||||
String randomVal = getRandomValue();
|
String randomVal = getRandomValue();
|
||||||
m.appendReplacement(sb, randomVal);
|
m.appendReplacement(sb, randomVal);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||||
import org.keycloak.representations.idm.EventRepresentation;
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
import org.keycloak.representations.idm.GroupRepresentation;
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
|
||||||
import org.keycloak.representations.idm.PartialImportRepresentation;
|
import org.keycloak.representations.idm.PartialImportRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
|
||||||
import org.keycloak.representations.idm.RealmRepresentation;
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
@ -222,6 +223,13 @@ public interface RealmResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
Response testLDAPConnection(TestLdapConnectionRepresentation config);
|
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")
|
@Path("testSMTPConnection")
|
||||||
@POST
|
@POST
|
||||||
@NoCache
|
@NoCache
|
||||||
|
|
|
@ -35,6 +35,9 @@ public class LDAPConstants {
|
||||||
public static final String VENDOR_TIVOLI = "tivoli";
|
public static final String VENDOR_TIVOLI = "tivoli";
|
||||||
public static final String VENDOR_NOVELL_EDIRECTORY="edirectory" ;
|
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 USERNAME_LDAP_ATTRIBUTE = "usernameLDAPAttribute";
|
||||||
public static final String RDN_LDAP_ATTRIBUTE = "rdnLDAPAttribute";
|
public static final String RDN_LDAP_ATTRIBUTE = "rdnLDAPAttribute";
|
||||||
public static final String UUID_LDAP_ATTRIBUTE = "uuidLDAPAttribute";
|
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_POOLING_TIMEOUT = "connectionPoolingTimeout";
|
||||||
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
|
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
|
||||||
public static final String READ_TIMEOUT = "readTimeout";
|
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 PAGINATION = "pagination";
|
||||||
|
|
||||||
public static final String EDIT_MODE = "editMode";
|
public static final String EDIT_MODE = "editMode";
|
||||||
|
@ -84,6 +88,7 @@ public class LDAPConstants {
|
||||||
// Custom user search filter
|
// Custom user search filter
|
||||||
public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter";
|
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";
|
public static final String START_TLS = "startTls";
|
||||||
|
|
||||||
// Custom attributes on UserModel, which is mapped to LDAP
|
// Custom attributes on UserModel, which is mapped to LDAP
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,21 +16,49 @@
|
||||||
*/
|
*/
|
||||||
package org.keycloak.services.resources.admin;
|
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.logging.Logger;
|
||||||
import org.jboss.resteasy.annotations.cache.NoCache;
|
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.jboss.resteasy.spi.ResteasyProviderFactory;
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.KeyPairVerifier;
|
import org.keycloak.KeyPairVerifier;
|
||||||
import org.keycloak.authentication.CredentialRegistrator;
|
import org.keycloak.authentication.CredentialRegistrator;
|
||||||
import org.keycloak.authentication.RequiredActionFactory;
|
|
||||||
import org.keycloak.authentication.RequiredActionProvider;
|
import org.keycloak.authentication.RequiredActionProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
import org.keycloak.common.util.PemUtils;
|
import org.keycloak.common.util.PemUtils;
|
||||||
import org.keycloak.credential.CredentialProvider;
|
|
||||||
import org.keycloak.email.EmailTemplateProvider;
|
import org.keycloak.email.EmailTemplateProvider;
|
||||||
import org.keycloak.events.Event;
|
import org.keycloak.events.Event;
|
||||||
import org.keycloak.events.EventQuery;
|
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.ExportOptions;
|
||||||
import org.keycloak.exportimport.util.ExportUtils;
|
import org.keycloak.exportimport.util.ExportUtils;
|
||||||
import org.keycloak.keys.PublicKeyStorageProvider;
|
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.CacheRealmProvider;
|
||||||
import org.keycloak.models.cache.UserCache;
|
import org.keycloak.models.cache.UserCache;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
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.representations.idm.TestLdapConnectionRepresentation;
|
||||||
import org.keycloak.services.ErrorResponse;
|
import org.keycloak.services.ErrorResponse;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
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.RealmManager;
|
||||||
import org.keycloak.services.managers.ResourceAdminManager;
|
import org.keycloak.services.managers.ResourceAdminManager;
|
||||||
import org.keycloak.services.managers.UserStorageSyncManager;
|
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.AdminPermissionManagement;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
import org.keycloak.storage.UserStorageProviderModel;
|
import org.keycloak.storage.UserStorageProviderModel;
|
||||||
|
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
|
||||||
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.utils.ReservedCharValidator;
|
import org.keycloak.utils.ReservedCharValidator;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base resource class for the admin REST api of one realm
|
* 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) {
|
@FormParam("componentId") String componentId, @FormParam("startTls") String startTls) {
|
||||||
auth.realm().requireManageRealm();
|
auth.realm().requireManageRealm();
|
||||||
|
|
||||||
if (componentId != null && bindCredential.equals(ComponentRepresentation.SECRET_VALUE)) {
|
TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls, LDAPConstants.AUTH_TYPE_SIMPLE);
|
||||||
bindCredential = realm.getComponent(componentId).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL);
|
config.setComponentId(componentId);
|
||||||
}
|
boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
|
||||||
|
|
||||||
boolean result = LDAPConnectionTestManager.testLDAP(session, action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls);
|
|
||||||
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
|
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -955,15 +966,28 @@ public class RealmAdminResource {
|
||||||
@NoCache
|
@NoCache
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
public Response testLDAPConnection(TestLdapConnectionRepresentation config) {
|
public Response testLDAPConnection(TestLdapConnectionRepresentation config) {
|
||||||
return testLDAPConnection(
|
boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
|
||||||
config.getAction(),
|
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
|
||||||
config.getConnectionUrl(),
|
}
|
||||||
config.getBindDn(),
|
|
||||||
config.getBindCredential(),
|
/**
|
||||||
config.getUseTruststoreSpi(),
|
* Get LDAP supported extensions.
|
||||||
config.getConnectionTimeout(),
|
* @param config LDAP configuration
|
||||||
config.getComponentId(),
|
* @return
|
||||||
config.getStartTls());
|
*/
|
||||||
|
@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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -616,6 +616,7 @@ public class UserResource {
|
||||||
} catch (ReadOnlyException mre) {
|
} catch (ReadOnlyException mre) {
|
||||||
throw new BadRequestException("Can't reset password as account is read only");
|
throw new BadRequestException("Can't reset password as account is read only");
|
||||||
} catch (ModelException e) {
|
} catch (ModelException e) {
|
||||||
|
logger.warn("Could not update user password.", e);
|
||||||
Properties messages = AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale());
|
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()),
|
throw new ErrorResponseException(e.getMessage(), MessageFormat.format(messages.getProperty(e.getMessage(), e.getMessage()), e.getParameters()),
|
||||||
Status.BAD_REQUEST);
|
Status.BAD_REQUEST);
|
||||||
|
|
|
@ -17,14 +17,21 @@
|
||||||
|
|
||||||
package org.keycloak.testsuite.admin;
|
package org.keycloak.testsuite.admin;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.ClassRule;
|
import org.junit.ClassRule;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.keycloak.models.LDAPConstants;
|
||||||
|
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
|
||||||
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
|
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.Assert;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
|
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
|
||||||
import org.keycloak.testsuite.util.LDAPRule;
|
import org.keycloak.testsuite.util.LDAPRule;
|
||||||
|
|
||||||
|
import javax.ws.rs.BadRequestException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
|
||||||
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
|
||||||
|
@ -46,30 +53,30 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
|
||||||
assertStatus(response, 400);
|
assertStatus(response, 400);
|
||||||
|
|
||||||
// Bad host
|
// 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);
|
assertStatus(response, 400);
|
||||||
|
|
||||||
// Connection success
|
// 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);
|
assertStatus(response, 204);
|
||||||
|
|
||||||
// Bad authentication
|
// 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);
|
assertStatus(response, 400);
|
||||||
|
|
||||||
// Authentication success
|
// 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);
|
assertStatus(response, 204);
|
||||||
|
|
||||||
// Authentication success with bindCredential from Vault
|
// 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);
|
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);
|
assertStatus(response, 204);
|
||||||
|
|
||||||
// Deprecated form based
|
// 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);
|
assertStatus(response, 204);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -77,26 +84,50 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
|
||||||
@Test
|
@Test
|
||||||
public void testLdapConnectionsSsl() {
|
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);
|
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);
|
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);
|
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);
|
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);
|
assertStatus(response, 204);
|
||||||
|
|
||||||
// Authentication success with bindCredential from Vault
|
// 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);
|
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) {
|
private void assertStatus(Response response, int status) {
|
||||||
Assert.assertEquals(status, response.getStatus());
|
Assert.assertEquals(status, response.getStatus());
|
||||||
response.close();
|
response.close();
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,9 @@ public class LdapUserProviderForm extends Form {
|
||||||
@FindBy(id = "searchScope")
|
@FindBy(id = "searchScope")
|
||||||
private Select searchScopeSelect;
|
private Select searchScopeSelect;
|
||||||
|
|
||||||
|
@FindBy(id = "kerberosIntegrationHeader")
|
||||||
|
private WebElement kerberosIntegrationHeader;
|
||||||
|
|
||||||
@FindBy(id = "kerberosRealm")
|
@FindBy(id = "kerberosRealm")
|
||||||
private WebElement kerberosRealmInput;
|
private WebElement kerberosRealmInput;
|
||||||
|
|
||||||
|
@ -123,7 +126,7 @@ public class LdapUserProviderForm extends Form {
|
||||||
@FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='changedSyncEnabled']]")
|
@FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='changedSyncEnabled']]")
|
||||||
private OnOffSwitch periodicChangedUsersSync;
|
private OnOffSwitch periodicChangedUsersSync;
|
||||||
|
|
||||||
@FindByJQuery("a:contains('Connection Pooling Settings')")
|
@FindBy(id = "connectionPoolSettingsHeader")
|
||||||
private WebElement connectionPoolingSettingsButton;
|
private WebElement connectionPoolingSettingsButton;
|
||||||
|
|
||||||
@FindBy(id = "connectionPoolingAuthentication")
|
@FindBy(id = "connectionPoolingAuthentication")
|
||||||
|
@ -191,6 +194,16 @@ public class LdapUserProviderForm extends Form {
|
||||||
UIUtils.setTextInputValue(customUserSearchFilterInput, customUserSearchFilter);
|
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) {
|
public void setKerberosRealmInput(String kerberosRealm) {
|
||||||
UIUtils.setTextInputValue(kerberosRealmInput, kerberosRealm);
|
UIUtils.setTextInputValue(kerberosRealmInput, kerberosRealm);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
|
||||||
createLdapUserProvider.form().setLdapBindCredentialInput("secret");
|
createLdapUserProvider.form().setLdapBindCredentialInput("secret");
|
||||||
// createLdapUserProvider.form().setAccountAfterPasswordUpdateEnabled(false);
|
// createLdapUserProvider.form().setAccountAfterPasswordUpdateEnabled(false);
|
||||||
// enable kerberos
|
// enable kerberos
|
||||||
|
createLdapUserProvider.form().uncollapseKerberosIntegrationHeader();
|
||||||
createLdapUserProvider.form().setAllowKerberosAuthEnabled(true);
|
createLdapUserProvider.form().setAllowKerberosAuthEnabled(true);
|
||||||
createLdapUserProvider.form().setKerberosRealmInput("KEYCLOAK.ORG");
|
createLdapUserProvider.form().setKerberosRealmInput("KEYCLOAK.ORG");
|
||||||
createLdapUserProvider.form().setServerPrincipalInput("HTTP/localhost@KEYCLOAK.ORG");
|
createLdapUserProvider.form().setServerPrincipalInput("HTTP/localhost@KEYCLOAK.ORG");
|
||||||
|
|
|
@ -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.
|
ldap.import-enabled.tooltip=If true, LDAP users will be imported into Keycloak DB and synced by the configured sync policies.
|
||||||
vendor=Vendor
|
vendor=Vendor
|
||||||
ldap.vendor.tooltip=LDAP vendor (provider)
|
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
|
username-ldap-attribute=Username LDAP attribute
|
||||||
ldap-attribute-name-for-username=LDAP attribute name for username
|
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.
|
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.
|
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.
|
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.
|
||||||
|
|
|
@ -1364,7 +1364,8 @@ module.controller('UserGroupMembershipCtrl', function($scope, $q, realm, user, U
|
||||||
});
|
});
|
||||||
|
|
||||||
module.controller('LDAPUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
|
module.controller('LDAPUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
|
||||||
serverInfo, instance, Components, UserStorageOperations, RealmLDAPConnectionTester) {
|
serverInfo, instance, Components, UserStorageOperations,
|
||||||
|
RealmLDAPConnectionTester, $http) {
|
||||||
console.log('LDAPUserStorageCtrl');
|
console.log('LDAPUserStorageCtrl');
|
||||||
var providerId = 'ldap';
|
var providerId = 'ldap';
|
||||||
console.log('providerId: ' + providerId);
|
console.log('providerId: ' + providerId);
|
||||||
|
@ -1672,10 +1673,12 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
|
||||||
Notifications.error("Error during unlink");
|
Notifications.error("Error during unlink");
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var initConnectionTest = function(testAction, ldapConfig) {
|
var initConnectionTest = function(testAction, ldapConfig) {
|
||||||
return {
|
return {
|
||||||
action: testAction,
|
action: testAction,
|
||||||
connectionUrl: ldapConfig.connectionUrl && ldapConfig.connectionUrl[0],
|
connectionUrl: ldapConfig.connectionUrl && ldapConfig.connectionUrl[0],
|
||||||
|
authType: ldapConfig.authType && ldapConfig.authType[0],
|
||||||
bindDn: ldapConfig.bindDn && ldapConfig.bindDn[0],
|
bindDn: ldapConfig.bindDn && ldapConfig.bindDn[0],
|
||||||
bindCredential: ldapConfig.bindCredential && ldapConfig.bindCredential[0],
|
bindCredential: ldapConfig.bindCredential && ldapConfig.bindCredential[0],
|
||||||
useTruststoreSpi: ldapConfig.useTruststoreSpi && ldapConfig.useTruststoreSpi[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."));
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -127,43 +127,6 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'ldap.users-dn.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'ldap.users-dn.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix">
|
||||||
<label class="col-md-2 control-label" for="customUserSearchFilter">{{:: 'custom-user-ldap-filter' | translate}}</label>
|
<label class="col-md-2 control-label" for="customUserSearchFilter">{{:: 'custom-user-ldap-filter' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -184,6 +147,60 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'ldap.search-scope.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'ldap.search-scope.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix">
|
||||||
<label class="col-md-2 control-label" for="validatePasswordPolicy">{{:: 'validate-password-policy' | translate}}</label>
|
<label class="col-md-2 control-label" for="validatePasswordPolicy">{{:: 'validate-password-policy' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -211,65 +228,6 @@
|
||||||
</div>
|
</div>
|
||||||
<kc-tooltip>{{:: 'ldap.use-truststore-spi.tooltip' | translate}}</kc-tooltip>
|
<kc-tooltip>{{:: 'ldap.use-truststore-spi.tooltip' | translate}}</kc-tooltip>
|
||||||
</div>
|
</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">
|
<div class="form-group clearfix">
|
||||||
<label class="col-md-2 control-label" for="connectionTimeout">{{:: 'ldap-connection-timeout' | translate}}</label>
|
<label class="col-md-2 control-label" for="connectionTimeout">{{:: 'ldap-connection-timeout' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -294,7 +252,67 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<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">
|
<div class="form-group">
|
||||||
<label class="col-md-2 control-label" for="allowKerberosAuthentication">{{:: 'allow-kerberos-authentication' | translate}} </label>
|
<label class="col-md-2 control-label" for="allowKerberosAuthentication">{{:: 'allow-kerberos-authentication' | translate}} </label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -340,7 +358,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<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">
|
<div class="form-group clearfix">
|
||||||
<label class="col-md-2 control-label" for="batchSizeForSync">{{:: 'batch-size' | translate}}</label>
|
<label class="col-md-2 control-label" for="batchSizeForSync">{{:: 'batch-size' | translate}}</label>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
@ -379,7 +397,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<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">
|
<div class="form-group">
|
||||||
<label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
|
<label for="cachePolicy" class="col-md-2 control-label">{{:: 'userStorage.cachePolicy' | translate}}</label>
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.lang.text.StrSubstitutor;
|
import org.apache.commons.lang.text.StrSubstitutor;
|
||||||
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
|
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.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.LdifEntry;
|
||||||
import org.apache.directory.api.ldap.model.ldif.LdifReader;
|
import org.apache.directory.api.ldap.model.ldif.LdifReader;
|
||||||
import org.apache.directory.api.ldap.model.schema.SchemaManager;
|
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.factory.JdbmPartitionFactory;
|
||||||
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
|
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
|
||||||
import org.apache.directory.server.ldap.LdapServer;
|
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.TcpTransport;
|
||||||
import org.apache.directory.server.protocol.shared.transport.Transport;
|
import org.apache.directory.server.protocol.shared.transport.Transport;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
@ -255,6 +257,13 @@ public class LDAPEmbeddedServer {
|
||||||
// Associate the DS to this LdapServer
|
// Associate the DS to this LdapServer
|
||||||
ldapServer.setDirectoryService( directoryService );
|
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
|
// Propagate the anonymous flag to the DS
|
||||||
directoryService.setAllowAnonymousAccess(false);
|
directoryService.setAllowAnonymousAccess(false);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue