io.vertigo.account.plugins.identityprovider.ldap.LdapIdentityProviderPlugin Maven / Gradle / Ivy
The newest version!
/*
* vertigo - application development platform
*
* Copyright (C) 2013-2024, Vertigo.io, [email protected]
*
* 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 io.vertigo.account.plugins.identityprovider.ldap;
import java.io.ByteArrayInputStream;
import java.io.Serializable;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import io.vertigo.account.impl.account.AccountMapperHelper;
import io.vertigo.account.impl.identityprovider.IdentityProviderPlugin;
import io.vertigo.commons.codec.CodecManager;
import io.vertigo.connectors.ldap.EsapiLdapEncoder;
import io.vertigo.connectors.ldap.LdapConnector;
import io.vertigo.core.lang.Assertion;
import io.vertigo.core.lang.WrappedException;
import io.vertigo.core.node.Node;
import io.vertigo.core.node.component.Activeable;
import io.vertigo.core.param.ParamValue;
import io.vertigo.datamodel.data.definitions.DataDefinition;
import io.vertigo.datamodel.data.definitions.DataField;
import io.vertigo.datamodel.data.model.Entity;
import io.vertigo.datamodel.data.model.UID;
import io.vertigo.datamodel.data.util.DataModelUtil;
import io.vertigo.datamodel.smarttype.SmartTypeManager;
import io.vertigo.datamodel.smarttype.definitions.FormatterException;
import io.vertigo.datastore.filestore.model.VFile;
import io.vertigo.datastore.impl.filestore.model.StreamFile;
/**
* Source of identity.
* @author npiedeloup
*/
public final class LdapIdentityProviderPlugin implements IdentityProviderPlugin, Activeable {
private static final int MAX_ROWS = 500;
private static final String LDAP_PHOTO_MIME_TYPE = "image/jpeg";
private static final String PHOTO_RESERVED_FIELD = "photo";
private static final Logger LOGGER = LogManager.getLogger(LdapIdentityProviderPlugin.class);
private final LdapConnector ldapConnector;
private final CodecManager codecManager;
private final SmartTypeManager smartTypeManager;
private final String ldapAccountBaseDn;
private final String ldapUserAuthAttribute;
private final String userIdentityEntity;
private final String ldapUserAttributeMappingStr;
private AccountMapperHelper mapperHelper;
/**
* Constructor.
* @param ldapServerHost Ldap Server host
* @param ldapServerPort Ldap server port (default : 389)
* @param ldapAccountBaseDn Base de recherche des DNs d'Accounts
* @param ldapReaderLogin Login du reader LDAP
* @param ldapReaderPassword Password du reader LDAP
* @param ldapUserAuthAttribute Ldap attribute use to find user by it's authToken
* @param userIdentityEntity DtDefinition used for User
* @param ldapUserAttributeMappingStr Mapping from LDAP to Account
* @param codecManager Codec Manager
*/
@Inject
public LdapIdentityProviderPlugin(
@ParamValue("ldapAccountBaseDn") final String ldapAccountBaseDn,
@ParamValue("ldapUserAuthAttribute") final String ldapUserAuthAttribute,
@ParamValue("userIdentityEntity") final String userIdentityEntity,
@ParamValue("ldapUserAttributeMapping") final String ldapUserAttributeMappingStr,
@ParamValue("connectorName") final Optional connectorNameOpt,
final CodecManager codecManager,
final SmartTypeManager smartTypeManager,
final List ldapConnectors) {
Assertion.check()
.isNotBlank(ldapAccountBaseDn)
.isNotBlank(ldapUserAuthAttribute)
.isNotBlank(userIdentityEntity)
.isNotBlank(ldapUserAttributeMappingStr)
.isNotNull(codecManager)
.isNotNull(smartTypeManager)
.isNotNull(ldapConnectors)
.isFalse(ldapConnectors.isEmpty(), "At least one LdapConnector espected");
//-----
this.ldapAccountBaseDn = ldapAccountBaseDn;
this.ldapUserAuthAttribute = ldapUserAuthAttribute;
this.userIdentityEntity = userIdentityEntity;
this.ldapUserAttributeMappingStr = ldapUserAttributeMappingStr;
this.smartTypeManager = smartTypeManager;
this.codecManager = codecManager;
final String connectorName = connectorNameOpt.orElse("main");
ldapConnector = ldapConnectors.stream()
.filter(connector -> connectorName.equals(connector.getName()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Can't found LdapConnector named '" + connectorName + "' in " + ldapConnectors));
}
/** {@inheritDoc} */
@Override
public void start() {
final DataDefinition userDtDefinition = Node.getNode().getDefinitionSpace().resolve(userIdentityEntity, DataDefinition.class);
mapperHelper = new AccountMapperHelper(userDtDefinition, ldapUserAttributeMappingStr)
.withReservedDestField(PHOTO_RESERVED_FIELD)
.parseAttributeMapping();
}
/** {@inheritDoc} */
@Override
public void stop() {
//rien
}
/** {@inheritDoc} */
@Override
public E getUserByAuthToken(final String userAuthToken) {
final LdapContext ldapContext = ldapConnector.getClient();
try {
return (E) getUserByAuthToken(userAuthToken, ldapContext);
} finally {
closeLdapContext(ldapContext);
}
}
/** {@inheritDoc} */
@Override
public long getUsersCount() {
throw new UnsupportedOperationException("Can't count all account from LDAP : anti-spooffing protections");
}
/** {@inheritDoc} */
@Override
public List getAllUsers() {
final LdapContext ldapContext = ldapConnector.getClient();
try {
return searchUser("(" + ldapUserAuthAttribute + "=*)", MAX_ROWS, ldapContext);
} finally {
closeLdapContext(ldapContext);
}
}
/** {@inheritDoc} */
@Override
public Optional getPhoto(final UID accountURI) {
final LdapContext ldapContext = ldapConnector.getClient();
try {
final String displayName = "photo-" + accountURI.getId() + ".jpg";
final String photoAttributeName = mapperHelper.getReservedSourceAttribute(PHOTO_RESERVED_FIELD);
return parseOptionalVFile(displayName, getLdapAttributes(accountURI.getId(), Collections.singleton(photoAttributeName), ldapContext));
} finally {
closeLdapContext(ldapContext);
}
}
private Entity getUserByAuthToken(final String authToken, final LdapContext ctx) {
final List result = searchLdapAttributes(ldapAccountBaseDn, "(&(" + ldapUserAuthAttribute + "=" + protectLdap(authToken) + "))", 2, mapperHelper.sourceAttributes(), ctx);
Assertion.check()
.isFalse(result.isEmpty(), "Can't found any user with authToken : {0}", ldapUserAuthAttribute)
.isTrue(result.size() == 1, "Too many user with same authToken ({0} shoud be unique)", ldapUserAuthAttribute);
return parseUser(result.get(0));
}
private List searchUser(final String searchRequest, final int top, final LdapContext ldapContext) {
final List result = searchLdapAttributes(ldapAccountBaseDn, searchRequest, top, mapperHelper.sourceAttributes(), ldapContext);
return (List) result.stream()
.map(this::parseUser)
.toList();
}
private Attributes getLdapAttributes(final Serializable accountId, final Set returningAttributes, final LdapContext ldapContext) {
final String ldapIdAttr = mapperHelper.getSourceIdField();
final List result = searchLdapAttributes(ldapAccountBaseDn, "(" + ldapIdAttr + "=" + accountId + ")", 2, returningAttributes, ldapContext);
Assertion.check()
.isFalse(result.isEmpty(), "Can't found any user with id : {0}", accountId)
.isTrue(result.size() == 1, "Too many user with same id ({0} shoud be unique)", accountId);
return result.get(0);
}
private Optional parseOptionalVFile(final String displayName, final Attributes attrs) {
final Object rawData = parseRawAttribute(mapperHelper.getReservedSourceAttribute(PHOTO_RESERVED_FIELD), attrs);
if (rawData == null) {
return Optional.empty();
}
if (rawData instanceof String) {
//si string alors base64
return Optional.of(base64toVFile(displayName, (String) rawData));
} else if (rawData instanceof byte[]) {
return Optional.of(byteArrayToVFile(displayName, (byte[]) rawData));
} else {
throw new IllegalArgumentException("Can't get photo " + mapperHelper.getReservedSourceAttribute(PHOTO_RESERVED_FIELD) + " from LDAP, format not supported " + rawData.getClass());
}
}
private static String parseNullableAttribute(final String attributeName, final Attributes attrs) {
if (attributeName != null) {
final Attribute attribute = attrs.get(attributeName);
if (attribute != null) {
try {
final Object value = attribute.get();
Assertion.check().isNotNull(value);
return String.valueOf(value);
} catch (final NamingException e) {
throw WrappedException.wrap(e, "Ldap attribute {0} found, but is empty", attributeName);
}
}
}
return null;
}
private static Object parseRawAttribute(final String attributeName, final Attributes attrs) {
if (attributeName != null) {
final Attribute attribute = attrs.get(attributeName);
if (attribute != null) {
try {
final Object value = attribute.get();
Assertion.check().isNotNull(value);
return value;
} catch (final NamingException e) {
throw WrappedException.wrap(e, "Ldap attribute {0} found, but is empty", attributeName);
}
}
}
return null;
}
private VFile base64toVFile(final String displayName, final String base64Content) {
final byte[] photo = codecManager.getBase64Codec().decode(base64Content);
return byteArrayToVFile(displayName, photo);
}
private static VFile byteArrayToVFile(final String displayName, final byte[] photo) {
return new StreamFile(displayName, LDAP_PHOTO_MIME_TYPE, Instant.now(), photo.length, () -> new ByteArrayInputStream(photo));
}
private static String protectLdap(final String principal) {
return EsapiLdapEncoder.encodeForDN(principal);
}
private static void closeLdapContext(final LdapContext ldapContext) {
try {
ldapContext.close();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("LDAP connection successfully \"{}\"", ldapContext);
}
} catch (final NamingException e) {
throw WrappedException.wrap(e, "Error when closing LdapContext");
}
}
private Entity parseUser(final Attributes attrs) {
try {
final Entity user = Entity.class.cast(DataModelUtil.createDataObject(mapperHelper.getDestDefinition()));
for (final DataField dtField : mapperHelper.destAttributes()) {
final String value = parseNullableAttribute(mapperHelper.getSourceAttribute(dtField), attrs);
if (value != null) {
setTypedValue(dtField, user, value);
}
}
return user;
} catch (final FormatterException e) {
throw WrappedException.wrap(e, "Can't parse Account from LDAP");
}
}
private void setTypedValue(final DataField dtField, final Entity user, final String valueStr) throws FormatterException {
final Serializable typedValue = (Serializable) smartTypeManager.stringToValue(dtField.smartTypeDefinition(), valueStr);
dtField.getDataAccessor().setValue(user, typedValue);
}
private static List searchLdapAttributes(final String ldapBaseDn, final String searchRequest, final int top, final Collection returningAttributes, final LdapContext ctx) {
final List userAttributes = new ArrayList<>();
final SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
constraints.setReturningAttributes(buildReturingAttributes(returningAttributes));
constraints.setCountLimit(top);
try {
//LDAP ctx.search can be vulnerable to LDAP injection : we already protect all user entries by EsapiLdapEncoder
final NamingEnumeration answer = ctx.search(ldapBaseDn, searchRequest, constraints);
while (answer.hasMore()) {
final Attributes attrs = answer.next().getAttributes();
userAttributes.add(attrs);
}
} catch (final NamingException e) {
throw WrappedException.wrap(e, "Can't search LDAP user with request: {0}", searchRequest);
}
return userAttributes;
}
private static String[] buildReturingAttributes(final Collection returningAttributes) {
Assertion.check().isNotNull(returningAttributes);
//-----
return returningAttributes.toArray(new String[returningAttributes.size()]);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy