Skip to content

Commit

Permalink
KEYCLOAK-10927 - Implement LDAPv3 Password Modify Extended Operation … (
Browse files Browse the repository at this point in the history
keycloak#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>
  • Loading branch information
4 people authored May 20, 2020
1 parent cc77620 commit 3382682
Show file tree
Hide file tree
Showing 24 changed files with 907 additions and 219 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@ public class TestLdapConnectionRepresentation {
private String connectionTimeout;
private String componentId;
private String startTls;
private String authType;

public TestLdapConnectionRepresentation() {
}

public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout) {
this(action, connectionUrl, bindDn, bindCredential, useTruststoreSpi, connectionTimeout, null, null);
}

public TestLdapConnectionRepresentation(String action, String connectionUrl, String bindDn, String bindCredential, String useTruststoreSpi, String connectionTimeout, String startTls, String authType) {
this.action = action;
this.connectionUrl = connectionUrl;
this.bindDn = bindDn;
this.bindCredential = bindCredential;
this.useTruststoreSpi = useTruststoreSpi;
this.connectionTimeout = connectionTimeout;
this.startTls = startTls;
this.authType = authType;
}

public String getAction() {
Expand All @@ -39,6 +46,14 @@ public void setConnectionUrl(String connectionUrl) {
this.connectionUrl = connectionUrl;
}

public String getAuthType() {
return authType;
}

public void setAuthType(String authType) {
this.authType = authType;
}

public String getBindDn() {
return bindDn;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ public String getAuthType() {
}
}

public boolean useExtendedPasswordModifyOp() {
String value = config.getFirst(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP);
return Boolean.parseBoolean(value);
}

public String getUseTruststoreSpi() {
return config.getFirst(LDAPConstants.USE_TRUSTSTORE_SPI);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapper;
import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapperFactory;
import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapper;
import org.keycloak.storage.ldap.mappers.LDAPStorageMapperFactory;
Expand Down Expand Up @@ -106,6 +108,9 @@ private static List<ProviderConfigProperty> getConfigProps(ComponentModel parent
.property().name(LDAPConstants.VENDOR)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
.property().name(LDAPConstants.USE_PASSWORD_MODIFY_EXTENDED_OP)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.add()
.property().name(LDAPConstants.USERNAME_LDAP_ATTRIBUTE)
.type(ProviderConfigProperty.STRING_TYPE)
.add()
Expand Down Expand Up @@ -308,6 +313,7 @@ public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel m
UserStorageProvider.EditMode editMode = ldapConfig.getEditMode();
String readOnly = String.valueOf(editMode == UserStorageProvider.EditMode.READ_ONLY || editMode == UserStorageProvider.EditMode.UNSYNCED);
String usernameLdapAttribute = ldapConfig.getUsernameLdapAttribute();
boolean syncRegistrations = Boolean.valueOf(model.getConfig().getFirst(LDAPConstants.SYNC_REGISTRATIONS));

String alwaysReadValueFromLDAP = String.valueOf(editMode== UserStorageProvider.EditMode.READ_ONLY || editMode== UserStorageProvider.EditMode.WRITABLE);

Expand Down Expand Up @@ -420,6 +426,15 @@ public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel m
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
}

// In case that "Sync Registration" is ON and the LDAP v3 Password-modify extension is ON, we will create hardcoded mapper to create
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
if (!activeDirectory && syncRegistrations && ldapConfig.useExtendedPasswordModifyOp()) {
mapperModel = KeycloakModelUtils.createComponentModel("random initial password", model.getId(), HardcodedLDAPAttributeMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName(),
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_NAME, LDAPConstants.USER_PASSWORD_ATTRIBUTE,
HardcodedLDAPAttributeMapper.LDAP_ATTRIBUTE_VALUE, HardcodedLDAPAttributeMapper.RANDOM_ATTRIBUTE_VALUE);
realm.addComponentModel(mapperModel);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@

package org.keycloak.storage.ldap.idm.store;

import java.util.Set;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;

Expand Down Expand Up @@ -93,6 +96,17 @@ public interface IdentityStore {
//
// <V extends Relationship> int countQueryResults(RelationshipQuery<V> query);

/**
* Query the LDAP server <a href="https://ldapwiki.com/wiki/RootDSE">RootDSE</a> and extract the {@link LDAPCapabilityRepresentation}
* of all supported <i>extensions</i>, <i>controls</i> and <i>features</i> the server announces. The LDAP Wiki
* provides a <a href="https://ldapwiki.com/wiki/LDAP%20Extensions%20and%20Controls%20Listing">list of known capabilities</a>.
*
* Will throw a {@link ModelException} on any LDAP error, or when the searchResult is empty.
*
* @return a set of LDAPOid, each representing a server capability (control, extension or feature).
*/
Set<LDAPCapabilityRepresentation> queryServerCapabilities();

// Credentials

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
import org.keycloak.models.LDAPConstants;
import org.keycloak.models.ModelException;
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation.CapabilityType;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.model.LDAPObject;
import org.keycloak.representations.idm.LDAPCapabilityRepresentation;
import org.keycloak.storage.ldap.idm.query.Condition;
import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
import org.keycloak.storage.ldap.idm.query.internal.EqualCondition;
Expand Down Expand Up @@ -306,6 +308,40 @@ public int countQueryResults(LDAPQuery identityQuery) {
return resultCount;
}

@Override
public Set<LDAPCapabilityRepresentation> queryServerCapabilities() {
Set<LDAPCapabilityRepresentation> result = new LinkedHashSet<>();
try {
List<String> attrs = new ArrayList<>();
attrs.add("supportedControl");
attrs.add("supportedExtension");
attrs.add("supportedFeatures");
List<SearchResult> searchResults = operationManager
.search("", "(objectClass=*)", Collections.unmodifiableCollection(attrs), SearchControls.OBJECT_SCOPE);
if (searchResults.size() != 1) {
throw new ModelException("Could not query root DSE: unexpected result size");
}
SearchResult rootDse = searchResults.get(0);
Attributes attributes = rootDse.getAttributes();
for (String attr: attrs) {
Attribute attribute = attributes.get(attr);
if (null != attribute) {
CapabilityType capabilityType = CapabilityType.fromRootDseAttributeName(attr);
NamingEnumeration<?> values = attribute.getAll();
while (values.hasMoreElements()) {
Object o = values.nextElement();
LDAPCapabilityRepresentation capability = new LDAPCapabilityRepresentation(o, capabilityType);
logger.info("rootDSE query: " + capability);
result.add(capability);
}
}
}
return result;
} catch (NamingException e) {
throw new ModelException("Failed to query root DSE: " + e.getMessage(), e);
}
}

// *************** CREDENTIALS AND USER SPECIFIC STUFF

@Override
Expand All @@ -329,24 +365,25 @@ public void updatePassword(LDAPObject user, String password, LDAPOperationDecora

if (getConfig().isActiveDirectory()) {
updateADPassword(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
return;
}

try {
try {
if (config.useExtendedPasswordModifyOp()) {
operationManager.passwordModifyExtended(userDN, password, passwordUpdateDecorator);
} else {
ModificationItem[] mods = new ModificationItem[1];
BasicAttribute mod0 = new BasicAttribute(LDAPConstants.USER_PASSWORD_ATTRIBUTE, password);

mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, mod0);

operationManager.modifyAttributes(userDN, mods, passwordUpdateDecorator);
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
} catch (ModelException me) {
throw me;
} catch (Exception e) {
throw new ModelException("Error updating password.", e);
}
}


private void updateADPassword(String userDN, String password, LDAPOperationDecorator passwordUpdateDecorator) {
try {
// Replace the "unicdodePwd" attribute with a new value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.keycloak.storage.ldap.LDAPConfig;
import org.keycloak.storage.ldap.idm.model.LDAPDn;
import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery;
import org.keycloak.storage.ldap.idm.store.ldap.extended.PasswordModifyRequest;
import org.keycloak.storage.ldap.mappers.LDAPOperationDecorator;

import javax.naming.AuthenticationException;
Expand Down Expand Up @@ -669,6 +670,25 @@ public String decodeEntryUUID(final Object entryUUID) {
return entryUUID.toString();
}

/**
* Execute the LDAP Password Modify Extended Operation to update the password for the given DN.
*
* @param dn distinguished name of the entry.
* @param password the new password.
* @param decorator A decorator to apply to the ldap operation.
*/

public void passwordModifyExtended(String dn, String password, LDAPOperationDecorator decorator) {
try {
execute(context -> {
PasswordModifyRequest modifyRequest = new PasswordModifyRequest(dn, null, password);
return context.extendedOperation(modifyRequest);
}, decorator);
} catch (NamingException e) {
throw new ModelException("Could not execute the password modify extended operation for DN [" + dn + "]", e);
}
}

private <R> R execute(LdapOperation<R> operation) throws NamingException {
return execute(operation, null);
}
Expand Down
Loading

0 comments on commit 3382682

Please sign in to comment.