operation) throws NamingException {
return execute(operation, null);
}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/extended/PasswordModifyRequest.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/extended/PasswordModifyRequest.java
new file mode 100644
index 0000000000..cb73ddf843
--- /dev/null
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/idm/store/ldap/extended/PasswordModifyRequest.java
@@ -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
+ *
+ * LDAP Password Modify Extended Operation
+ *
+ * client request.
+ *
+ * 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
+ *
+ * BER encoding
+ * ; 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);
+ }
+ }
+}
diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPAttributeMapper.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPAttributeMapper.java
index 035c623713..220f779665 100644
--- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPAttributeMapper.java
+++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/mappers/HardcodedLDAPAttributeMapper.java
@@ -41,6 +41,13 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
public static final String LDAP_ATTRIBUTE_VALUE = "ldap.attribute.value";
+ private static final String RANDOM = "RANDOM";
+
+ /**
+ * When this is configured as LDAP_ATTRIBUTE_VALUE, the mapper will use randomly generated value
+ */
+ public static final String RANDOM_ATTRIBUTE_VALUE = "${" + RANDOM + "}";
+
public static Pattern substitution = Pattern.compile("\\$\\{([^}]+)\\}");
public HardcodedLDAPAttributeMapper(ComponentModel mapperModel, LDAPStorageProvider ldapProvider) {
@@ -65,7 +72,7 @@ public class HardcodedLDAPAttributeMapper extends AbstractLDAPStorageMapper {
while (m.find()) {
String token = m.group(1);
- if (token.equals("RANDOM")) {
+ if (token.equals(RANDOM)) {
String randomVal = getRandomValue();
m.appendReplacement(sb, randomVal);
} else {
diff --git a/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPCapabilityTest.java b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPCapabilityTest.java
new file mode 100644
index 0000000000..3843c7820f
--- /dev/null
+++ b/federation/ldap/src/test/java/org/keycloak/storage/ldap/idm/model/LDAPCapabilityTest.java
@@ -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 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);
+ }
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
index 41b6a49718..635f0f7698 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java
@@ -24,6 +24,7 @@ import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.representations.idm.PartialImportRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -222,6 +223,13 @@ public interface RealmResource {
@NoCache
Response testLDAPConnection(TestLdapConnectionRepresentation config);
+ @POST
+ @Path("ldap-server-capabilities")
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
+ List ldapServerCapabilities(TestLdapConnectionRepresentation config);
+
@Path("testSMTPConnection")
@POST
@NoCache
diff --git a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
index f001ba9090..d2fc898b87 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/LDAPConstants.java
@@ -35,6 +35,9 @@ public class LDAPConstants {
public static final String VENDOR_TIVOLI = "tivoli";
public static final String VENDOR_NOVELL_EDIRECTORY="edirectory" ;
+ // Could be discovered by rootDse supportedExtension: 1.3.6.1.4.1.4203.1.11.1
+ public static final String USE_PASSWORD_MODIFY_EXTENDED_OP = "usePasswordModifyExtendedOp";
+
public static final String USERNAME_LDAP_ATTRIBUTE = "usernameLDAPAttribute";
public static final String RDN_LDAP_ATTRIBUTE = "rdnLDAPAttribute";
public static final String UUID_LDAP_ATTRIBUTE = "uuidLDAPAttribute";
@@ -66,6 +69,7 @@ public class LDAPConstants {
public static final String CONNECTION_POOLING_TIMEOUT = "connectionPoolingTimeout";
public static final String CONNECTION_TIMEOUT = "connectionTimeout";
public static final String READ_TIMEOUT = "readTimeout";
+ // Could be discovered by rootDse supportedControl: 1.2.840.113556.1.4.319
public static final String PAGINATION = "pagination";
public static final String EDIT_MODE = "editMode";
@@ -84,6 +88,7 @@ public class LDAPConstants {
// Custom user search filter
public static final String CUSTOM_USER_SEARCH_FILTER = "customUserSearchFilter";
+ // Could be discovered by rootDse supportedExtension: 1.3.6.1.4.1.1466.20037
public static final String START_TLS = "startTls";
// Custom attributes on UserModel, which is mapped to LDAP
diff --git a/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java b/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java
deleted file mode 100755
index e378a52c91..0000000000
--- a/services/src/main/java/org/keycloak/services/managers/LDAPConnectionTestManager.java
+++ /dev/null
@@ -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 Marek Posolda
- */
-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 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;
- }
- }
-}
diff --git a/services/src/main/java/org/keycloak/services/managers/LDAPServerCapabilitiesManager.java b/services/src/main/java/org/keycloak/services/managers/LDAPServerCapabilitiesManager.java
new file mode 100755
index 0000000000..41f51bd497
--- /dev/null
+++ b/services/src/main/java/org/keycloak/services/managers/LDAPServerCapabilitiesManager.java
@@ -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 Marek Posolda
+ */
+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 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 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;
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index 70e527ca84..274b59ac94 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -16,21 +16,49 @@
*/
package org.keycloak.services.resources.admin;
-import com.fasterxml.jackson.core.type.TypeReference;
+import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
+import static org.keycloak.util.JsonSerialization.readValue;
+
+import java.security.cert.X509Certificate;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
-import javax.ws.rs.BadRequestException;
-import javax.ws.rs.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.Config;
import org.keycloak.KeyPairVerifier;
import org.keycloak.authentication.CredentialRegistrator;
-import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
-import org.keycloak.credential.CredentialProvider;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Event;
import org.keycloak.events.EventQuery;
@@ -45,7 +73,18 @@ import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.exportimport.util.ExportOptions;
import org.keycloak.exportimport.util.ExportUtils;
import org.keycloak.keys.PublicKeyStorageProvider;
-import org.keycloak.models.*;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientScopeModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RequiredActionProviderModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.UserCache;
import org.keycloak.models.utils.KeycloakModelUtils;
@@ -69,7 +108,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.AuthenticationManager;
-import org.keycloak.services.managers.LDAPConnectionTestManager;
+import org.keycloak.services.managers.LDAPServerCapabilitiesManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.UserStorageSyncManager;
@@ -77,37 +116,11 @@ import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluato
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.storage.UserStorageProviderModel;
-
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.FormParam;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-import java.security.cert.X509Certificate;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.keycloak.models.utils.StripSecretsUtils.stripForExport;
-import static org.keycloak.util.JsonSerialization.readValue;
+import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.utils.ReservedCharValidator;
+import com.fasterxml.jackson.core.type.TypeReference;
+
/**
* Base resource class for the admin REST api of one realm
*
@@ -938,11 +951,9 @@ public class RealmAdminResource {
@FormParam("componentId") String componentId, @FormParam("startTls") String startTls) {
auth.realm().requireManageRealm();
- if (componentId != null && bindCredential.equals(ComponentRepresentation.SECRET_VALUE)) {
- bindCredential = realm.getComponent(componentId).getConfig().getFirst(LDAPConstants.BIND_CREDENTIAL);
- }
-
- boolean result = LDAPConnectionTestManager.testLDAP(session, action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls);
+ TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, startTls, LDAPConstants.AUTH_TYPE_SIMPLE);
+ config.setComponentId(componentId);
+ boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
}
@@ -955,15 +966,28 @@ public class RealmAdminResource {
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
public Response testLDAPConnection(TestLdapConnectionRepresentation config) {
- return testLDAPConnection(
- config.getAction(),
- config.getConnectionUrl(),
- config.getBindDn(),
- config.getBindCredential(),
- config.getUseTruststoreSpi(),
- config.getConnectionTimeout(),
- config.getComponentId(),
- config.getStartTls());
+ boolean result = LDAPServerCapabilitiesManager.testLDAP(config, session, realm);
+ return result ? Response.noContent().build() : ErrorResponse.error("LDAP test error", Response.Status.BAD_REQUEST);
+ }
+
+ /**
+ * Get LDAP supported extensions.
+ * @param config LDAP configuration
+ * @return
+ */
+ @POST
+ @Path("ldap-server-capabilities")
+ @NoCache
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(javax.ws.rs.core.MediaType.APPLICATION_JSON)
+ public Response ldapServerCapabilities(TestLdapConnectionRepresentation config) {
+ auth.realm().requireManageRealm();
+ try {
+ Set ldapCapabilities = LDAPServerCapabilitiesManager.queryServerCapabilities(config, session, realm);
+ return Response.ok().entity(ldapCapabilities).build();
+ } catch (Exception e) {
+ return ErrorResponse.error("ldapServerCapabilities error", Status.BAD_REQUEST);
+ }
}
/**
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
index 8fff8eaacd..4faf7a671f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UserResource.java
@@ -616,6 +616,7 @@ public class UserResource {
} catch (ReadOnlyException mre) {
throw new BadRequestException("Can't reset password as account is read only");
} catch (ModelException e) {
+ logger.warn("Could not update user password.", e);
Properties messages = AdminRoot.getMessages(session, realm, auth.adminAuth().getToken().getLocale());
throw new ErrorResponseException(e.getMessage(), MessageFormat.format(messages.getProperty(e.getMessage(), e.getMessage()), e.getParameters()),
Status.BAD_REQUEST);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserFederationLdapConnectionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserFederationLdapConnectionTest.java
index 836c0014f2..f3c8e44098 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserFederationLdapConnectionTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/UserFederationLdapConnectionTest.java
@@ -17,14 +17,21 @@
package org.keycloak.testsuite.admin;
+import java.util.List;
+
+import org.hamcrest.Matchers;
import org.junit.ClassRule;
import org.junit.Test;
+import org.keycloak.models.LDAPConstants;
+import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.representations.idm.TestLdapConnectionRepresentation;
-import org.keycloak.services.managers.LDAPConnectionTestManager;
+import org.keycloak.services.managers.LDAPServerCapabilitiesManager;
+import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.util.LDAPRule;
+import javax.ws.rs.BadRequestException;
import javax.ws.rs.core.Response;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer;
@@ -46,30 +53,30 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
assertStatus(response, 400);
// Bad host
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldap://localhostt:10389", "foo", "bar", "false", null));
assertStatus(response, 400);
// Connection success
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldap://localhost:10389", "foo", "bar", "false", null, "false", LDAPConstants.AUTH_TYPE_NONE));
assertStatus(response, 204);
// Bad authentication
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false", "10000"));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "foo", "bar", "false", "10000"));
assertStatus(response, 400);
// Authentication success
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "secret", "false", null));
assertStatus(response, 204);
// Authentication success with bindCredential from Vault
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
assertStatus(response, 204);
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null));
assertStatus(response, 204);
// Deprecated form based
- response = realm.testLDAPConnection(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null);
+ response = realm.testLDAPConnection(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldap://localhost:10389", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "false", null);
assertStatus(response, 204);
}
@@ -77,26 +84,50 @@ public class UserFederationLdapConnectionTest extends AbstractAdminTest {
@Test
public void testLdapConnectionsSsl() {
- Response response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false", null));
+ Response response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldaps://localhost:10636", "foo", "bar", "false", null, null, LDAPConstants.AUTH_TYPE_NONE));
assertStatus(response, 204);
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_CONNECTION, "ldaps://localhostt:10636", "foo", "bar", "false", null));
assertStatus(response, 400);
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "foo", "bar", "false", null));
assertStatus(response, 400);
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", null));
assertStatus(response, 204);
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000"));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "secret", "true", "10000"));
assertStatus(response, 204);
// Authentication success with bindCredential from Vault
- response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPConnectionTestManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "true", null));
+ response = realm.testLDAPConnection(new TestLdapConnectionRepresentation(LDAPServerCapabilitiesManager.TEST_AUTHENTICATION, "ldaps://localhost:10636", "uid=admin,ou=system", "${vault.ldap_bindCredential}", "true", null));
assertStatus(response, 204);
}
+ @Test
+ public void testLdapCapabilities() {
+
+ // Query the rootDSE success
+ TestLdapConnectionRepresentation config = new TestLdapConnectionRepresentation(
+ LDAPServerCapabilitiesManager.QUERY_SERVER_CAPABILITIES, "ldap://localhost:10389", "uid=admin,ou=system", "secret",
+ "false", null, "false", LDAPConstants.AUTH_TYPE_SIMPLE);
+
+ List ldapCapabilities = realm.ldapServerCapabilities(config);
+ Assert.assertThat(ldapCapabilities, Matchers.hasItem(new LDAPCapabilityRepresentation(PasswordModifyRequest.PASSWORD_MODIFY_OID, LDAPCapabilityRepresentation.CapabilityType.EXTENSION)));
+
+ // Query the rootDSE failure
+ try {
+ config = new TestLdapConnectionRepresentation(
+ LDAPServerCapabilitiesManager.QUERY_SERVER_CAPABILITIES, "ldap://localhost:10389", "foo", "bar",
+ "false", null, "false", LDAPConstants.AUTH_TYPE_SIMPLE);
+ realm.ldapServerCapabilities(config);
+
+ Assert.fail("It wasn't expected to successfully sent the request for query capabilities");
+ } catch (BadRequestException bre) {
+ // Expected
+ }
+ }
+
private void assertStatus(Response response, int status) {
Assert.assertEquals(status, response.getStatus());
response.close();
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPPasswordModifyExtensionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPPasswordModifyExtensionTest.java
new file mode 100644
index 0000000000..94f17002a0
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/ldap/LDAPPasswordModifyExtensionTest.java
@@ -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 Marek Posolda
+ */
+@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);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
index 1c00ea1dec..0805a9f9f7 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/federation/LdapUserProviderForm.java
@@ -60,6 +60,9 @@ public class LdapUserProviderForm extends Form {
@FindBy(id = "searchScope")
private Select searchScopeSelect;
+ @FindBy(id = "kerberosIntegrationHeader")
+ private WebElement kerberosIntegrationHeader;
+
@FindBy(id = "kerberosRealm")
private WebElement kerberosRealmInput;
@@ -123,7 +126,7 @@ public class LdapUserProviderForm extends Form {
@FindBy(xpath = ".//div[contains(@class,'onoffswitch') and ./input[@id='changedSyncEnabled']]")
private OnOffSwitch periodicChangedUsersSync;
- @FindByJQuery("a:contains('Connection Pooling Settings')")
+ @FindBy(id = "connectionPoolSettingsHeader")
private WebElement connectionPoolingSettingsButton;
@FindBy(id = "connectionPoolingAuthentication")
@@ -191,6 +194,16 @@ public class LdapUserProviderForm extends Form {
UIUtils.setTextInputValue(customUserSearchFilterInput, customUserSearchFilter);
}
+ public void uncollapseKerberosIntegrationHeader() {
+ if (UIUtils.isElementVisible(kerberosRealmInput)) {
+ // Already collapsed
+ return;
+ }
+
+ kerberosIntegrationHeader.click();
+ waitUntilElement(By.id("kerberosRealm")).is().present();
+ }
+
public void setKerberosRealmInput(String kerberosRealm) {
UIUtils.setTextInputValue(kerberosRealmInput, kerberosRealm);
}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
index d65829ae1b..37b36c021f 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/federation/LdapUserFederationTest.java
@@ -45,6 +45,7 @@ public class LdapUserFederationTest extends AbstractConsoleTest {
createLdapUserProvider.form().setLdapBindCredentialInput("secret");
// createLdapUserProvider.form().setAccountAfterPasswordUpdateEnabled(false);
// enable kerberos
+ createLdapUserProvider.form().uncollapseKerberosIntegrationHeader();
createLdapUserProvider.form().setAllowKerberosAuthEnabled(true);
createLdapUserProvider.form().setKerberosRealmInput("KEYCLOAK.ORG");
createLdapUserProvider.form().setServerPrincipalInput("HTTP/localhost@KEYCLOAK.ORG");
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index a1ee33bd90..f7d05075cb 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -945,6 +945,8 @@ import-enabled=Import Users
ldap.import-enabled.tooltip=If true, LDAP users will be imported into Keycloak DB and synced by the configured sync policies.
vendor=Vendor
ldap.vendor.tooltip=LDAP vendor (provider)
+enable-usePasswordModifyExtendedOp=Enable the LDAPv3 Password Modify Extended Operation
+ldap.usePasswordModifyExtendedOp.tooltip=Use the LDAPv3 Password Modify Extended Operation (RFC-3062). The password modify extended operation usually requires that LDAP user already has password in the LDAP server. So when this is used with 'Sync Registrations', it can be good to add also 'Hardcoded LDAP attribute mapper' with randomly generated initial password.
username-ldap-attribute=Username LDAP attribute
ldap-attribute-name-for-username=LDAP attribute name for username
username-ldap-attribute.tooltip=Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server vendors it can be 'uid'. For Active directory it can be 'sAMAccountName' or 'cn'. The attribute should be filled for all LDAP user records you want to import from LDAP to Keycloak.
@@ -1623,3 +1625,8 @@ pkce-code-challenge-method=Proof Key for Code Exchange Code Challenge Method
pkce-code-challenge-method.tooltip=Choose which code challenge method for PKCE is used. If not specified, keycloak does not applies PKCE to a client unless the client sends an authorization request with appropriate code challenge and code exchange method.
key-not-allowed-here=Key '{{character}}' is not allowed here.
+
+# KEYCLOAK-10927 Implement LDAPv3 Password Modify Extended Operation
+advanced-ldap-settings=Advanced Settings
+ldap-query-supported-extensions=Query Supported Extensions
+ldap-query-supported-extensions.tooltip=This will query LDAP server for supported extensions, controls and features. Some advanced settings of the LDAP provider will be then automatically configured based on the capabilities/extensions/features supported by LDAP server. For example if LDAPv3 Password Modify extension is supported by LDAP server, corresponding switch will be enabled for LDAP provider.
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index 75e84ce1e3..82bd5f9545 100755
--- a/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -14,7 +14,7 @@ module.controller('UserRoleMappingCtrl', function($scope, $http, $route, realm,
$scope.clientMappings = [];
$scope.dummymodel = [];
$scope.selectedClient = null;
-
+
$scope.realmMappings = RealmRoleMapping.query({realm : realm.realm, userId : user.id});
$scope.realmRoles = AvailableRealmRoleMapping.query({realm : realm.realm, userId : user.id});
@@ -697,7 +697,7 @@ module.controller('UserCredentialsCtrl', function($scope, realm, user, $route, $
var msgTitle = ($scope.hasPassword ? 'Reset' : 'Set') + ' password';
var msg = 'Are you sure you want to ' + ($scope.hasPassword ? 'reset' : 'set') + ' a password for the user?';
var msgSuccess = 'The password has been ' + ($scope.hasPassword ? 'reset.' : 'set.');
-
+
Dialog.confirm(msgTitle, msg, function() {
UserCredentials.resetPassword({ realm: realm.realm, userId: user.id }, { type : "password", value : $scope.password, temporary: $scope.temporaryPassword }, function() {
Notifications.success(msgSuccess);
@@ -1364,7 +1364,8 @@ module.controller('UserGroupMembershipCtrl', function($scope, $q, realm, user, U
});
module.controller('LDAPUserStorageCtrl', function($scope, $location, Notifications, $route, Dialog, realm,
- serverInfo, instance, Components, UserStorageOperations, RealmLDAPConnectionTester) {
+ serverInfo, instance, Components, UserStorageOperations,
+ RealmLDAPConnectionTester, $http) {
console.log('LDAPUserStorageCtrl');
var providerId = 'ldap';
console.log('providerId: ' + providerId);
@@ -1672,10 +1673,12 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
Notifications.error("Error during unlink");
});
};
+
var initConnectionTest = function(testAction, ldapConfig) {
return {
action: testAction,
connectionUrl: ldapConfig.connectionUrl && ldapConfig.connectionUrl[0],
+ authType: ldapConfig.authType && ldapConfig.authType[0],
bindDn: ldapConfig.bindDn && ldapConfig.bindDn[0],
bindCredential: ldapConfig.bindCredential && ldapConfig.bindCredential[0],
useTruststoreSpi: ldapConfig.useTruststoreSpi && ldapConfig.useTruststoreSpi[0],
@@ -1703,7 +1706,23 @@ module.controller('LDAPUserStorageCtrl', function($scope, $location, Notificatio
});
}
+ $scope.queryAndSetLdapSupportedExtensions = function() {
+ console.log('LDAPCtrl: getLdapSupportedExtensions');
+ const PASSWORD_MODIFY_OID = '1.3.6.1.4.1.4203.1.11.1';
+ $http.post(
+ `${authUrl}/admin/realms/${realm.realm}/ldap-server-capabilities`,
+ initConnectionTest("queryServerCapabilities", $scope.instance.config)).then(
+ (response) => {
+ Notifications.success("LDAP supported extensions successfully requested.");
+ const ldapOids = response.data;
+ if (angular.isArray(ldapOids)) {
+ const passwordModifyOid = ldapOids.filter(ldapOid => ldapOid.oid === PASSWORD_MODIFY_OID);
+ $scope.instance.config['usePasswordModifyExtendedOp'][0] = `${passwordModifyOid.length > 0}`;
+ }
+ },
+ () => Notifications.error("Error when trying to request supported extensions of LDAP. See server.log for details."));
+ }
});
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html
index c57d10de94..6e250223d6 100755
--- a/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-storage-ldap.html
@@ -127,43 +127,6 @@
{{:: 'ldap.users-dn.tooltip' | translate}}
-
-
-
-
+
+
+
+
+
+