org.wildfly.security.auth.realm.ldap.LdapSecurityRealm Maven / Gradle / Ivy
The newest version!
/*
* JBoss, Home of Professional Open Source
* Copyright 2014 Red Hat, Inc., and individual 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.wildfly.security.auth.realm.ldap;
import static org.wildfly.security.auth.realm.ldap.ElytronMessages.log;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.security.Provider;
import java.security.spec.AlgorithmParameterSpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.naming.Binding;
import javax.naming.InvalidNameException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.ReferralException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.event.EventContext;
import javax.naming.event.NamespaceChangeListener;
import javax.naming.event.NamingEvent;
import javax.naming.event.NamingExceptionEvent;
import javax.naming.event.ObjectChangeListener;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;
import javax.naming.ldap.Rdn;
import org.wildfly.common.Assert;
import org.wildfly.common.function.ExceptionSupplier;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.realm.CacheableSecurityRealm;
import org.wildfly.security.auth.realm.IdentitySharedExclusiveLock;
import org.wildfly.security.auth.realm.IdentitySharedExclusiveLock.IdentityLock;
import org.wildfly.security.auth.server.ModifiableRealmIdentityIterator;
import org.wildfly.security.auth.server.ModifiableRealmIdentity;
import org.wildfly.security.auth.server.ModifiableSecurityRealm;
import org.wildfly.security.auth.server.NameRewriter;
import org.wildfly.security.auth.server.RealmIdentity;
import org.wildfly.security.auth.server.RealmUnavailableException;
import org.wildfly.security.auth.SupportLevel;
import org.wildfly.security.authz.AuthorizationIdentity;
import org.wildfly.security.authz.MapAttributes;
import org.wildfly.security.credential.AlgorithmCredential;
import org.wildfly.security.credential.Credential;
import org.wildfly.security.evidence.AlgorithmEvidence;
import org.wildfly.security.evidence.Evidence;
import org.wildfly.security.password.spec.Encoding;
/**
* Security realm implementation backed by LDAP.
*
* @author Darran Lofthouse
* @author Jan Kalina
*/
class LdapSecurityRealm implements ModifiableSecurityRealm, CacheableSecurityRealm {
private static final String ENV_BINARY_ATTRIBUTES = "java.naming.ldap.attributes.binary";
private final Supplier providers;
private final ExceptionSupplier dirContextSupplier;
private final NameRewriter nameRewriter;
private final IdentityMapping identityMapping;
private final int pageSize;
private final Charset hashCharset;
private final Encoding hashEncoding;
private final List credentialLoaders;
private final List credentialPersisters;
private final List evidenceVerifiers;
private final ConcurrentHashMap realmIdentityLocks = new ConcurrentHashMap<>();
private Set> listenersPendingRegistration = new LinkedHashSet>();
LdapSecurityRealm(final Supplier providers,
final ExceptionSupplier dirContextSupplier,
final NameRewriter nameRewriter,
final IdentityMapping identityMapping,
final List credentialLoaders,
final List credentialPersisters,
final List evidenceVerifiers,
final int pageSize,
final Charset hashCharset,
final Encoding hashEncoding) {
this.providers = providers;
this.dirContextSupplier = dirContextSupplier;
this.nameRewriter = nameRewriter;
this.identityMapping = identityMapping;
this.pageSize = pageSize;
this.hashCharset = hashCharset != null ? hashCharset : StandardCharsets.UTF_8;
this.hashEncoding = hashEncoding != null ? hashEncoding : Encoding.BASE64;
this.credentialLoaders = credentialLoaders;
this.credentialPersisters = credentialPersisters;
this.evidenceVerifiers = evidenceVerifiers;
}
@Override
public RealmIdentity getRealmIdentity(final Principal principal) {
return getRealmIdentity(principal, false);
}
@Override
public ModifiableRealmIdentity getRealmIdentityForUpdate(final Principal principal) {
return getRealmIdentity(principal, true);
}
@Override
public void registerIdentityChangeListener(Consumer listener) {
synchronized (this.listenersPendingRegistration) {
DirContext dirContext = null;
try {
dirContext = obtainContext();
registerIdentityChangeListener(dirContext, listener);
} catch (Exception cause) {
// either connection died or realm not available during boot
// we need to wait, lets store
listenersPendingRegistration.add(listener);
log.ldapRealmDeferRegistration();
if (log.isDebugEnabled()) {
log.debug("Listener registration failure: ", cause);
}
} finally {
if (dirContext != null) {
closeContext(dirContext);
}
}
}
}
private void registerIdentityChangeListener(final DirContext dirContext, final Consumer listener) throws NamingException {
EventContext eventContext = (EventContext) dirContext.lookup("");
eventContext.addNamingListener("", EventContext.SUBTREE_SCOPE, new ServerNotificationListener(listener));
}
private ModifiableRealmIdentity getRealmIdentity(final Principal principal, final boolean exclusive) {
if (! NamePrincipal.isConvertibleTo(principal)) {
return ModifiableRealmIdentity.NON_EXISTENT;
}
String name = nameRewriter.rewriteName(principal.getName());
if (name == null) {
throw log.invalidName();
}
// Acquire the appropriate lock for the realm identity
log.debugf("Obtaining lock for identity [%s]...", name);
IdentitySharedExclusiveLock realmIdentityLock = getRealmIdentityLockForName(name);
IdentityLock lock;
if (exclusive) {
lock = realmIdentityLock.lockExclusive();
} else {
lock = realmIdentityLock.lockShared();
}
log.debugf("Obtained lock for identity [%s].", name);
return new LdapRealmIdentity(name, lock, hashCharset, hashEncoding);
}
private DirContext obtainContext() throws RealmUnavailableException {
try {
DirContext ctx = dirContextSupplier.get();
synchronized (this.listenersPendingRegistration) {
// we got ctx, this means connection is up
// add & remove in case we are ok, take into account network failure.
final Iterator> it = this.listenersPendingRegistration.iterator();
while(it.hasNext()) {
registerIdentityChangeListener(ctx, it.next());
it.remove();
}
return ctx;
}
} catch (NamingException e) {
throw log.ldapRealmFailedToObtainContext(e);
}
}
private void closeContext(DirContext dirContext) {
try {
dirContext.close();
} catch (NamingException e) {
log.debug("LdapSecurityRealm failed to close DirContext", e);
}
}
@Override
public ModifiableRealmIdentityIterator getRealmIdentityIterator() throws RealmUnavailableException {
if (identityMapping.iteratorFilter == null) {
throw log.ldapRealmNotConfiguredToSupportIteratingOverIdentities();
}
final DirContext dirContext = obtainContext();
final Stream resultStream;
final LdapSearch ldapSearch = new LdapSearch(identityMapping.searchDn, identityMapping.searchRecursive, pageSize, identityMapping.iteratorFilter);
ldapSearch.setReturningAttributes(Collections.singleton(identityMapping.rdnIdentifier));
resultStream = ldapSearch.search(dirContext);
Iterator iterator = resultStream.map(entry -> {
try {
return (String) entry.getAttributes().get(identityMapping.rdnIdentifier).get();
} catch (NamingException e) {
throw new RuntimeException(log.ldapRealmIdentitySearchFailed(e));
}
}).distinct().iterator(); // distinct to prevent deadlock on identity locking when one identity found twice
return new ModifiableRealmIdentityIterator() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public ModifiableRealmIdentity next() {
String name = iterator.next();
return getRealmIdentityForUpdate(new NamePrincipal(name));
}
@Override
public void close() throws RealmUnavailableException {
resultStream.close();
closeContext(dirContext);
}
};
}
@Override
public SupportLevel getCredentialAcquireSupport(final Class extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
Assert.checkNotNullParam("credentialType", credentialType);
SupportLevel response = SupportLevel.UNSUPPORTED;
for (CredentialLoader loader : credentialLoaders) {
SupportLevel support = loader.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec);
if (support.isDefinitelySupported()) {
// One claiming it is definitely supported is enough!
return support;
}
if (response.compareTo(support) < 0) {
response = support;
}
}
return response;
}
@Override
public SupportLevel getEvidenceVerifySupport(final Class extends Evidence> evidenceType, final String algorithmName) throws RealmUnavailableException {
Assert.checkNotNullParam("evidenceType", evidenceType);
SupportLevel response = SupportLevel.UNSUPPORTED;
for (EvidenceVerifier verifier : evidenceVerifiers) {
SupportLevel support = verifier.getEvidenceVerifySupport(evidenceType, algorithmName);
if (support.isDefinitelySupported()) {
// One claiming it is definitely supported is enough!
return support;
}
if (response.compareTo(support) < 0) {
response = support;
}
}
return response;
}
private IdentitySharedExclusiveLock getRealmIdentityLockForName(final String name) {
IdentitySharedExclusiveLock realmIdentityLock = realmIdentityLocks.get(name);
if (realmIdentityLock == null) {
final IdentitySharedExclusiveLock newRealmIdentityLock = new IdentitySharedExclusiveLock();
realmIdentityLock = realmIdentityLocks.putIfAbsent(name, newRealmIdentityLock);
if (realmIdentityLock == null) {
realmIdentityLock = newRealmIdentityLock;
}
}
return realmIdentityLock;
}
private class LdapRealmIdentity implements ModifiableRealmIdentity {
private final String name;
private IdentityLock lock;
private final Charset hashCharset;
private final Encoding hashEncoding;
LdapRealmIdentity(final String name, final IdentityLock lock, final Charset hashCharset, final Encoding hashEncoding) {
this.name = name;
this.lock = lock;
this.hashCharset = hashCharset;
this.hashEncoding = hashEncoding;
}
public Principal getRealmIdentityPrincipal() {
return new NamePrincipal(name);
}
public Charset getHashCharset() {
return this.hashCharset;
}
@Override
public SupportLevel getCredentialAcquireSupport(final Class extends Credential> credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
Assert.checkNotNullParam("credentialType", credentialType);
if (LdapSecurityRealm.this.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec) == SupportLevel.UNSUPPORTED) {
// If not supported in general then definitely not supported for a specific principal.
return SupportLevel.UNSUPPORTED;
}
DirContext dirContext = obtainContext();
try {
Set attributes = new HashSet<>();
Set binaryAttributes = new HashSet<>();
for (CredentialLoader loader : credentialLoaders) {
loader.addRequiredIdentityAttributes(attributes);
loader.addBinaryIdentityAttributes(binaryAttributes);
}
LdapIdentity identity = getIdentity(dirContext, attributes, binaryAttributes);
if (identity == null) {
return SupportLevel.UNSUPPORTED;
}
SupportLevel support = SupportLevel.UNSUPPORTED;
for (CredentialLoader loader : credentialLoaders) {
if (loader.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec).mayBeSupported()) {
IdentityCredentialLoader icl = loader.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getEntry().getAttributes(), hashEncoding);
SupportLevel temp = icl.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec, providers);
if (temp != null && temp.isDefinitelySupported()) {
// As soon as one claims definite support we know it is supported.
return temp;
}
if (temp != null && support.compareTo(temp) < 0) {
support = temp;
}
}
}
return support;
} finally {
closeContext(dirContext);
}
}
@Override
public C getCredential(final Class credentialType) throws RealmUnavailableException {
return getCredential(credentialType, null);
}
@Override
public C getCredential(final Class credentialType, final String algorithmName) throws RealmUnavailableException {
return getCredential(credentialType, algorithmName, null);
}
@Override
public C getCredential(final Class credentialType, final String algorithmName, final AlgorithmParameterSpec parameterSpec) throws RealmUnavailableException {
Assert.checkNotNullParam("credentialType", credentialType);
if (LdapSecurityRealm.this.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec) == SupportLevel.UNSUPPORTED) {
// If not supported in general then definitely not supported for a specific principal.
return null;
}
DirContext dirContext = obtainContext();
try {
Set attributes = new HashSet<>();
Set binaryAttributes = new HashSet<>();
for (CredentialLoader loader : credentialLoaders) {
loader.addRequiredIdentityAttributes(attributes);
loader.addBinaryIdentityAttributes(binaryAttributes);
}
LdapIdentity identity = getIdentity(dirContext, attributes, binaryAttributes);
if (identity == null) {
return null;
}
for (CredentialLoader loader : credentialLoaders) {
if (loader.getCredentialAcquireSupport(credentialType, algorithmName, parameterSpec).mayBeSupported()) {
IdentityCredentialLoader icl = loader.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getEntry().getAttributes(), hashEncoding);
Credential credential = icl.getCredential(credentialType, algorithmName, parameterSpec, providers);
if (credentialType.isInstance(credential)) {
return credentialType.cast(credential);
}
}
}
} finally {
closeContext(dirContext);
}
return null;
}
@Override
public void setCredentials(final Collection extends Credential> credentials) throws RealmUnavailableException {
Assert.checkNotNullParam("credentials", credentials);
DirContext dirContext = obtainContext();
try {
Set attributes = new HashSet<>();
Set binaryAttributes = new HashSet<>();
for (CredentialPersister persister : credentialPersisters) {
persister.addRequiredIdentityAttributes(attributes);
persister.addBinaryIdentityAttributes(binaryAttributes);
}
LdapIdentity identity = getIdentity(dirContext, attributes, binaryAttributes);
if (identity == null) {
throw log.ldapRealmIdentityNotExists(name);
}
// verify support
for (Credential credential : credentials) {
final Class extends Credential> credentialType = credential.getClass();
final String algorithmName = credential instanceof AlgorithmCredential ? ((AlgorithmCredential) credential).getAlgorithm() : null;
final AlgorithmParameterSpec parameterSpec = credential instanceof AlgorithmCredential ? ((AlgorithmCredential) credential).getParameters() : null;
boolean supported = false;
for (CredentialPersister persister : credentialPersisters) {
IdentityCredentialPersister icp = persister.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getEntry().getAttributes());
if (icp.getCredentialPersistSupport(credentialType, algorithmName, parameterSpec)) {
supported = true;
}
}
if (!supported) {
throw log.ldapRealmsPersisterNotSupported();
}
}
// clear
for (CredentialPersister persister : credentialPersisters) {
IdentityCredentialPersister icp = persister.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getEntry().getAttributes());
icp.clearCredentials();
}
// set
for (Credential credential : credentials) {
final Class extends Credential> credentialType = credential.getClass();
final String algorithmName = credential instanceof AlgorithmCredential ? ((AlgorithmCredential) credential).getAlgorithm() : null;
final AlgorithmParameterSpec parameterSpec = credential instanceof AlgorithmCredential ? ((AlgorithmCredential) credential).getParameters() : null;
for (CredentialPersister persister : credentialPersisters) {
IdentityCredentialPersister icp = persister.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getEntry().getAttributes());
if (icp.getCredentialPersistSupport(credentialType, algorithmName, parameterSpec)) {
icp.persistCredential(credential);
// next credential
break;
}
}
}
} finally {
closeContext(dirContext);
}
}
@Override
public void dispose() {
// Release the lock for this realm identity
IdentityLock identityLock = lock;
lock = null;
if (identityLock != null) {
identityLock.release();
}
}
@Override
public AuthorizationIdentity getAuthorizationIdentity() throws RealmUnavailableException {
return AuthorizationIdentity.basicIdentity(getAttributes());
}
@Override
public org.wildfly.security.authz.Attributes getAttributes() throws RealmUnavailableException {
DirContext context = obtainContext();
try {
LdapIdentity identity = getIdentity(context,
identityMapping.attributes.stream()
.map(AttributeMapping::getIdentityLdapName)
.filter(Objects::nonNull)
.collect(Collectors.toSet()),
null);
SearchResult entry = identity != null ? identity.getEntry() : null;
DirContext identityContext = identity != null ? identity.getDirContext() : null;
MapAttributes attributes = new MapAttributes();
attributes.addAll(extractSimpleAttributes(entry));
attributes.addAll(extractFilteredAttributes(entry, context, identityContext));
if (log.isDebugEnabled()) {
log.debugf("Obtaining authorization identity attributes for principal [%s]:", name);
if (attributes.isEmpty()) {
log.debugf("Identity [%s] does not have any attributes.", name);
} else {
log.debugf("Identity [%s] attributes are:", name);
attributes.keySet().forEach(key -> {
org.wildfly.security.authz.Attributes.Entry values = attributes.get(key);
values.forEach(value -> log.debugf(" Attribute [%s] value [%s].", key, value));
});
}
}
return attributes.asReadOnly();
} finally {
closeContext(context);
}
}
@Override
public SupportLevel getEvidenceVerifySupport(final Class extends Evidence> evidenceType, final String algorithmName) throws RealmUnavailableException {
Assert.checkNotNullParam("evidenceType", evidenceType);
DirContext dirContext = obtainContext();
try {
Set attributes = new HashSet<>();
Set binaryAttributes = new HashSet<>();
for (EvidenceVerifier verifier : evidenceVerifiers) {
verifier.addRequiredIdentityAttributes(attributes);
verifier.addBinaryIdentityAttributes(binaryAttributes);
}
LdapIdentity identity = getIdentity(dirContext, attributes, binaryAttributes);
if (identity == null) {
return SupportLevel.UNSUPPORTED;
}
SupportLevel response = SupportLevel.UNSUPPORTED;
for (EvidenceVerifier verifier : evidenceVerifiers) {
if (verifier.getEvidenceVerifySupport(evidenceType, algorithmName).mayBeSupported()) {
final IdentityEvidenceVerifier iev = verifier.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getUrl(), identity.getEntry().getAttributes(), hashEncoding);
final SupportLevel support = iev.getEvidenceVerifySupport(evidenceType, algorithmName, providers);
if (support != null && support.isDefinitelySupported()) {
// As soon as one claims definite support we know it is supported.
return support;
}
if (support != null && response.compareTo(support) < 0) {
response = support;
}
}
}
return response;
} finally {
closeContext(dirContext);
}
}
@Override
public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableException {
Assert.checkNotNullParam("evidence", evidence);
final Class extends Evidence> evidenceType = evidence.getClass();
final String algorithmName = evidence instanceof AlgorithmEvidence ? ((AlgorithmEvidence) evidence).getAlgorithm() : null;
if (LdapSecurityRealm.this.getEvidenceVerifySupport(evidenceType, algorithmName) == SupportLevel.UNSUPPORTED) {
// If not supported in general then definitely not supported for a specific principal.
return false;
}
DirContext dirContext = obtainContext();
try {
Set attributes = new HashSet<>();
Set binaryAttributes = new HashSet<>();
for (EvidenceVerifier verifier : evidenceVerifiers) {
verifier.addRequiredIdentityAttributes(attributes);
verifier.addBinaryIdentityAttributes(binaryAttributes);
}
LdapIdentity identity = getIdentity(dirContext, attributes, binaryAttributes);
if (identity == null) {
return false;
}
for (EvidenceVerifier verifier : evidenceVerifiers) {
if (verifier.getEvidenceVerifySupport(evidenceType, algorithmName).mayBeSupported()) {
IdentityEvidenceVerifier iev = verifier.forIdentity(identity.getDirContext(), identity.getDistinguishedName(), identity.getUrl(), identity.getEntry().getAttributes(), hashEncoding);
if (iev.verifyEvidence(evidence, providers, hashCharset)) {
return true;
}
}
}
} finally {
closeContext(dirContext);
}
return false;
}
@Override
public boolean exists() throws RealmUnavailableException {
DirContext dirContext = obtainContext();
try {
LdapIdentity identity = getIdentity(dirContext);
boolean exists = identity != null;
if (!exists) {
log.debugf("Principal [%s] does not exists.", name);
}
return exists;
} finally {
closeContext(dirContext);
}
}
private LdapSearch createLdapSearchByDn() {
if ( ! name.regionMatches(true, 0, identityMapping.rdnIdentifier, 0, identityMapping.rdnIdentifier.length())) {
return null;
} // equal sign not checked here as whitespaces can be between yet
try {
LdapName ldapName = new LdapName(name);
int rdnPosition = ldapName.size() - 1;
Rdn rdnIdentifier = ldapName.getRdn(rdnPosition);
if ( ! rdnIdentifier.getType().equalsIgnoreCase(identityMapping.rdnIdentifier)) { // uid=...
log.tracef("Getting identity [%s] by DN skipped - RDN does not match [%s]", name, identityMapping.rdnIdentifier);
return null;
}
if (identityMapping.searchDn != null) {
List expectedStart = new LdapName(identityMapping.searchDn).getRdns();
if ( ! ldapName.startsWith(expectedStart)) { // ...,search-dn
log.tracef("Getting identity [%s] by DN skipped - DN not in search-dn [%s]", name, identityMapping.searchDn);
return null;
}
if ( ! identityMapping.searchRecursive && ldapName.size() != expectedStart.size() + 1) {
log.tracef("Getting identity [%s] by DN skipped - DN not directly in search-dn and recursive search not enabled [%s]", name, identityMapping.searchDn);
return null;
}
}
return new LdapSearch(ldapName.toString(), SearchControls.OBJECT_SCOPE, 0, identityMapping.filterName, rdnIdentifier.getValue().toString());
} catch (InvalidNameException e) {
log.tracef(e, "Getting identity [%s] by DN failed - will continue by name", name);
}
return null;
}
private LdapIdentity getIdentity(DirContext dirContext) throws RealmUnavailableException {
return getIdentity(dirContext, null, null);
}
private LdapIdentity getIdentity(DirContext dirContext, Collection returningAttributes, Collection binaryAttributes) throws RealmUnavailableException {
log.debugf("Trying to create identity for principal [%s].", name);
LdapSearch ldapSearch = createLdapSearchByDn();
if (ldapSearch == null) { // name is not a valid DN, search by name
if (identityMapping.searchDn != null) {
ldapSearch = new LdapSearch(identityMapping.searchDn, identityMapping.searchRecursive, 0, identityMapping.filterName, name);
} else {
log.debugf("Identity for principal [%s] not found. The name is not a valid DN and the search base DN is null", name);
return null;
}
}
ldapSearch.setReturningAttributes(returningAttributes);
ldapSearch.setBinaryAttributes(binaryAttributes);
final LdapSearch ldapSearchFinal = ldapSearch;
try (Stream resultsStream = ldapSearch.search(dirContext)) {
SearchResult result = resultsStream.findFirst().orElse(null);
if (result != null) {
LdapIdentity identity = new LdapIdentity(name, ldapSearchFinal.getContext(), result.getNameInNamespace(), result.isRelative() ? null : result.getName(), result);
log.debugf("Identity for principal [%s] found at [%s].", name, identity.getDistinguishedName());
return identity;
} else {
log.debugf("Identity for principal [%s] not found.", name);
return null;
}
}
}
private String extractRdn(AttributeMapping mapping, final String dn) {
String valueRdn = mapping.getRdn();
try {
LdapName dnName = new LdapName(dn);
// loop RDNs in reverse order, left to right, to return the leftmost one that matches
for (int i = dnName.size() - 1; i >= 0; i--) {
Rdn rdn = dnName.getRdn(i);
if (rdn.getType().equalsIgnoreCase(valueRdn)) {
return rdn.getValue().toString();
}
}
} catch (Exception cause) {
throw log.ldapRealmInvalidRdnForAttribute(mapping.getName(), dn, valueRdn, cause);
}
return null;
}
/**
* Obtains attribute value by mapping from given entry and put it into given collection.
*
* @param entry LDAP entry, from which should be values obtained
* @param mapping attribute mapping defining attribute, whose values should be obtained
* @param outputCollection output collection for obtained values
* @return {@code true} if {@code outputCollection} was changed.
*/
private boolean valuesFromAttribute(SearchResult entry, AttributeMapping mapping, Collection outputCollection) throws NamingException {
if (mapping.getLdapName() == null) {
String value = entry.getNameInNamespace();
if (mapping.getRdn() != null) {
value = extractRdn(mapping, value);
}
return outputCollection.add(value);
} else {
Attributes entryAttributes = entry.getAttributes();
javax.naming.directory.Attribute ldapAttribute = entryAttributes.get(mapping.getLdapName());
if (ldapAttribute == null) return false;
NamingEnumeration> attributesEnum = null;
try {
attributesEnum = ldapAttribute.getAll();
Stream values = Collections.list(attributesEnum).stream().map(Object::toString);
if (mapping.getRdn() != null) {
values = values.map(val -> extractRdn(mapping, val)).filter(Objects::nonNull);
}
return values.map(outputCollection::add).filter(changed -> changed).count() != 0;
} finally {
if (attributesEnum != null) {
try {
attributesEnum.close();
} catch (NamingException ignore) {
}
}
}
}
}
private Map> extractFilteredAttributes(SearchResult identityEntry, DirContext context, DirContext identityContext) {
return extractAttributes(AttributeMapping::isFilteredOrReference, mapping -> {
Collection values = new HashSet<>();
final String searchDn = mapping.getSearchDn() != null ? mapping.getSearchDn() : identityMapping.searchDn;
List toSearch = new LinkedList<>();
toSearch.add(identityEntry);
for (int depth = 0; depth <= mapping.getRoleRecursionDepth() && ! toSearch.isEmpty(); depth++) {
List toSearchInNextLevel = new LinkedList<>();
for(SearchResult entry : toSearch) {
final String entryDn = entry != null ? entry.getNameInNamespace() : null;
if (mapping.getReference() != null && entry != null) { // reference
forEachAttributeValue(entry, mapping.getReference(), value -> {
LdapSearch search = new LdapSearch(value);
extractFilteredAttributesFromSearch(search, entry, mapping, context, identityContext, values, toSearchInNextLevel);
});
} else if (mapping.getReference() == null) { // filter
if (depth == 0) { // roles of identity
LdapSearch search = new LdapSearch(searchDn, mapping.getRecursiveSearch(), 0, mapping.getFilter(), name, entryDn);
extractFilteredAttributesFromSearch(search, entry, mapping, context, identityContext, values, toSearchInNextLevel);
} else if (entry != null) { // roles of role
forEachAttributeValue(entry, mapping.getRoleRecursionName(), roleName -> {
LdapSearch search = new LdapSearch(searchDn, mapping.getRecursiveSearch(), 0, mapping.getFilter(), roleName, entryDn);
extractFilteredAttributesFromSearch(search, entry, mapping, context, identityContext, values, toSearchInNextLevel);
});
}
}
}
toSearch = toSearchInNextLevel;
}
return values;
});
}
private void extractFilteredAttributesFromSearch(LdapSearch search, SearchResult referencedEntry, AttributeMapping mapping, DirContext context, DirContext identityContext, Collection identityAttributeValues, Collection toSearchInNextLevel) {
String referencedDn = referencedEntry != null ? referencedEntry.getNameInNamespace() : null;
Set attributes = new HashSet<>();
attributes.add(mapping.getLdapName());
attributes.add(mapping.getReference());
attributes.add(mapping.getRoleRecursionName());
search.setReturningAttributes(attributes);
try (Stream entries = search.search(mapping.searchInIdentityContext() ? identityContext : context)) {
entries.forEach(entry -> {
try {
if (valuesFromAttribute(entry, mapping, identityAttributeValues)) {
toSearchInNextLevel.add(entry);
}
} catch (Exception cause) {
throw ElytronMessages.log.ldapRealmFailedObtainAttributes(referencedDn, cause);
}
});
} catch (Exception cause) {
throw ElytronMessages.log.ldapRealmFailedObtainAttributes(referencedDn, cause);
}
}
private Map> extractSimpleAttributes(SearchResult identityEntry) {
if (identityEntry == null) return Collections.emptyMap();
return extractAttributes(mapping -> !mapping.isFilteredOrReference(), mapping -> {
Collection identityAttributeValues = new ArrayList<>();
try {
valuesFromAttribute(identityEntry, mapping, identityAttributeValues);
} catch (Exception cause) {
throw ElytronMessages.log.ldapRealmFailedObtainAttributes(identityEntry.getNameInNamespace(), cause);
}
return identityAttributeValues;
});
}
private Map> extractAttributes(Predicate filter, Function> valueFunction) {
return identityMapping.attributes.stream()
.filter(filter)
.collect(Collectors.toMap(AttributeMapping::getName, valueFunction, (values1, values2) -> {
List merged = new ArrayList<>(values1);
merged.addAll(values2);
return merged;
}));
}
private void forEachAttributeValue(SearchResult entry, String attrId, Consumer action) {
NamingEnumeration> attributesEnum = null;
try {
Attribute attribute = entry.getAttributes().get(attrId);
if (attribute == null) return;
attributesEnum = attribute.getAll();
Collections.list(attributesEnum).stream().map(Object::toString).forEach(action);
} catch (NamingException e) {
throw ElytronMessages.log.ldapRealmFailedObtainAttributes(entry.getNameInNamespace(), e);
} finally {
if (attributesEnum != null) {
try {
attributesEnum.close();
} catch (NamingException e) {
log.trace("Unable to close attributesEnum", e);
}
}
}
}
@Override
public void delete() throws RealmUnavailableException {
DirContext context = obtainContext();
try {
LdapIdentity identity = getIdentity(context);
if (identity == null) {
throw log.noSuchIdentity();
}
log.debugf("Removing identity [%s] with DN [%s] from LDAP", name, identity.getDistinguishedName());
identity.getDirContext().destroySubcontext(new LdapName(identity.getDistinguishedName()));
} catch (NamingException e) {
throw log.ldapRealmFailedDeleteIdentityFromServer(e);
} finally {
closeContext(context);
}
}
@Override
public void create() throws RealmUnavailableException {
if (identityMapping.newIdentityParent == null || identityMapping.newIdentityAttributes == null) {
throw log.ldapRealmNotConfiguredToSupportCreatingIdentities();
}
DirContext context = obtainContext();
try {
LdapName distinguishName = (LdapName) identityMapping.newIdentityParent.clone();
distinguishName.add(new Rdn(identityMapping.rdnIdentifier, name));
log.debugf("Creating identity [%s] with DN [%s] in LDAP", name, distinguishName.toString());
context.createSubcontext(distinguishName, identityMapping.newIdentityAttributes);
} catch (NamingException e) {
throw log.ldapRealmFailedCreateIdentityOnServer(e);
} finally {
closeContext(context);
}
}
@Override public void setAttributes(org.wildfly.security.authz.Attributes attributes) throws RealmUnavailableException {
log.debugf("Trying to set attributes for principal [%s].", name);
DirContext context = obtainContext();
try {
LdapIdentity identity = getIdentity(context);
if (identity == null) {
throw log.noSuchIdentity();
}
List modItems = new LinkedList<>();
LdapName identityLdapName = new LdapName(identity.getDistinguishedName());
String renameTo = null;
for(AttributeMapping mapping : identityMapping.attributes) {
if (mapping.getFilter() != null || mapping.getReference() != null || mapping.getRdn() != null) { // read-only mapping
if (attributes.size(mapping.getName()) != 0) {
log.ldapRealmDoesNotSupportSettingFilteredAttribute(mapping.getName(), name);
}
} else if (identityMapping.rdnIdentifier.equalsIgnoreCase(mapping.getLdapName())) { // entry rename
if (attributes.size(mapping.getName()) == 1) {
renameTo = attributes.get(mapping.getName(), 0);
} else {
throw log.ldapRealmRequiresExactlyOneRdnAttribute(mapping.getName(), name);
}
} else { // standard ldap attributes
if (attributes.size(mapping.getName()) == 0) {
BasicAttribute attribute = new BasicAttribute(mapping.getLdapName());
modItems.add(new ModificationItem(DirContext.REMOVE_ATTRIBUTE, attribute));
} else {
BasicAttribute attribute = new BasicAttribute(mapping.getLdapName());
attributes.get(mapping.getName()).forEach(attribute::add);
modItems.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attribute));
}
}
}
for(org.wildfly.security.authz.Attributes.Entry entry : attributes.entries()) {
if (identityMapping.attributes.stream().filter(mp -> mp.getName().equals(entry.getKey())).count() == 0) {
throw log.ldapRealmCannotSetAttributeWithoutMapping(entry.getKey(), name);
}
}
ModificationItem[] modItemsArray = modItems.toArray(new ModificationItem[modItems.size()]);
identity.getDirContext().modifyAttributes(identityLdapName, modItemsArray);
if (renameTo != null && ! renameTo.equals(identityLdapName.getRdn(identityLdapName.size()-1).getValue())) {
LdapName newLdapName = new LdapName(identityLdapName.getRdns().subList(0, identityLdapName.size()-1));
newLdapName.add(new Rdn(identityMapping.rdnIdentifier, renameTo));
identity.getDirContext().rename(identityLdapName, newLdapName);
}
} catch (Exception e) {
throw log.ldapRealmAttributesSettingFailed(name, e);
} finally {
closeContext(context);
}
}
private class LdapIdentity {
private final String name;
private final DirContext dirContext;
private final String distinguishedName;
private final String url;
private final SearchResult entry;
LdapIdentity(String name, DirContext dirContext, String distinguishedName, String url, SearchResult entry) {
this.name = name;
this.dirContext = dirContext;
this.distinguishedName = distinguishedName;
this.url = url;
this.entry = entry;
}
String getName() {
return this.name;
}
DirContext getDirContext() {
return this.dirContext;
}
String getDistinguishedName() {
return this.distinguishedName;
}
String getUrl() {
return this.url;
}
SearchResult getEntry() {
return this.entry;
}
}
}
private class LdapSearch {
private static final String NO_FILTER = "(objectclass=*)";
private final String searchDn;
private final int searchScope;
private final int pageSize;
private final String filter;
private final String[] filterArgs;
private Collection returningAttributes;
private Collection binaryAttributes;
private DirContext context;
private NamingEnumeration result;
private byte[] cookie;
private ReferralException referralException;
public LdapSearch(String searchDn, boolean searchRecursive, int pageSize, String filter, String... filterArgs) {
this(searchDn, searchRecursive ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE, pageSize, filter, filterArgs);
}
public LdapSearch(String searchDn, int searchScope, int pageSize, String filter, String... filterArgs) {
this.searchDn = searchDn;
this.searchScope = searchScope;
this.pageSize = pageSize;
this.filter = filter;
this.filterArgs = filterArgs;
}
public LdapSearch(String searchDn) {
this.searchDn = searchDn;
this.searchScope = SearchControls.OBJECT_SCOPE;
this.pageSize = 0;
this.filter = NO_FILTER;
this.filterArgs = null;
}
public Stream search(DirContext ctx) throws RealmUnavailableException {
if (log.isDebugEnabled()) {
log.debugf("Executing search [%s] in context [%s] with arguments [%s]. Returning attributes are [%s]. Binary attributes are [%s].",
filter, searchDn,
filterArgs == null ? null : String.join(", ", filterArgs),
returningAttributes == null ? null : String.join(", ", returningAttributes),
binaryAttributes == null ? null : String.join(", ", binaryAttributes)
);
}
context = ctx;
cookie = null;
try {
try {
result = searchWithPagination();
} catch (ReferralException e) {
referralException = e;
}
return StreamSupport.stream(new Spliterators.AbstractSpliterator(Long.MAX_VALUE, Spliterator.NONNULL) {
boolean finished = false;
Set