/** * This file is part of git-as-svn. It is subject to the license terms * in the LICENSE file found in the top-level directory of this distribution * and at http://www.gnu.org/licenses/gpl-2.0.html. No part of git-as-svn, * including this file, may be copied, modified, propagated, or distributed * except according to the terms contained in the LICENSE file. */ package svnserver.auth.ldap; import com.unboundid.ldap.sdk.*; import com.unboundid.util.ssl.SSLUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNException; import svnserver.auth.Authenticator; import svnserver.auth.PlainAuthenticator; import svnserver.auth.User; import svnserver.auth.UserDB; import svnserver.auth.ldap.config.LdapBind; import svnserver.auth.ldap.config.LdapUserDBConfig; import svnserver.config.ConfigHelper; import svnserver.context.SharedContext; import javax.naming.NamingException; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import javax.xml.bind.DatatypeConverter; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.GeneralSecurityException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.Collection; import java.util.Collections; /** * LDAP authentication. * * @author Artem V. Navrotskiy <bozaro@users.noreply.github.com> */ public class LdapUserDB implements UserDB { @NotNull private static final Logger log = LoggerFactory.getLogger(LdapUserDB.class); @NotNull private final Collection<Authenticator> authenticators = Collections.singleton(new PlainAuthenticator(this)); @NotNull private final LDAPConnectionPool pool; @NotNull private final LdapUserDBConfig config; @NotNull private final String baseDn; @Nullable private final String fakeMailSuffix; @FunctionalInterface private interface LdapCheck { boolean check(@NotNull String userDN) throws LDAPException; } @FunctionalInterface private interface LdapTask<T> { @Nullable T exec(@NotNull LDAPConnection connection) throws LDAPException; } public LdapUserDB(@NotNull SharedContext context, @NotNull LdapUserDBConfig config) { try { URI ldapUri = URI.create(config.getConnectionUrl()); this.baseDn = ldapUri.getPath().isEmpty() ? "" : ldapUri.getPath().substring(1); final LDAPConnection ldap = createConnection(context, config); final LdapBind bind = config.getBind(); if (bind != null) { bind.bind(ldap); } this.pool = new LDAPConnectionPool(ldap, 1, config.getMaxConnections()); this.fakeMailSuffix = createFakeMailSuffix(config); this.config = config; } catch (LDAPException e) { throw new IllegalStateException(e); } } @NotNull @Override public Collection<Authenticator> authenticators() { return authenticators; } @Nullable @Override public User check(@NotNull String userName, @NotNull String password) throws SVNException, IOException { return findUser(userName, userDN -> doTask(connection -> connection.bind(userDN, password).getResultCode() == ResultCode.SUCCESS)); } @Nullable @Override public User lookupByUserName(@NotNull String userName) throws SVNException, IOException { return findUser(userName, userDN -> true); } @Nullable @Override public User lookupByExternal(@NotNull String external) throws SVNException, IOException { return null; } @Nullable private String getAttribute(@NotNull SearchResultEntry entry, @NotNull String name) throws NamingException { Attribute attribute = entry.getAttribute(name); return attribute == null ? null : attribute.getValue(); } private <T> T doTask(@NotNull LdapTask<T> task) throws LDAPException { final LDAPConnection connection = pool.getConnection(); try { return task.exec(connection); } finally { connection.close(); } } private User findUser(@NotNull String userName, @NotNull LdapCheck ldapCheck) throws SVNException { try { final Filter filter; if (!config.getSearchFilter().isEmpty()) { filter = Filter.createANDFilter( Filter.create(config.getSearchFilter()), Filter.createEqualityFilter(config.getLoginAttribute(), userName) ); } else { filter = Filter.createEqualityFilter(config.getLoginAttribute(), userName); } final SearchResult search = doTask((connection) -> connection.search(baseDn, SearchScope.SUB, filter, config.getLoginAttribute(), config.getNameAttribute(), config.getEmailAttribute())); if (search.getEntryCount() == 1) { final SearchResultEntry entry = search.getSearchEntries().get(0); final String login = getAttribute(entry, config.getLoginAttribute()); if (login == null) { throw new IllegalStateException("Can't get login for user: " + userName); } if (ldapCheck.check(entry.getDN())) { final String realName = getAttribute(entry, config.getNameAttribute()); String email = getAttribute(entry, config.getEmailAttribute()); if (email == null && fakeMailSuffix != null) { email = login + fakeMailSuffix; } return User.create(login, realName != null ? realName : login, email, null); } } return null; } catch (LDAPException e) { if (e.getResultCode() == ResultCode.INVALID_CREDENTIALS) { return null; } throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_NO_PROVIDER, e.getMessage()), e); } catch (NamingException e) { throw new SVNException(SVNErrorMessage.create(SVNErrorCode.AUTHN_NO_PROVIDER, e.getMessage()), e); } } @Nullable private static String createFakeMailSuffix(@NotNull LdapUserDBConfig config) { final String suffix = config.getFakeMailSuffix(); if (suffix.isEmpty()) { return null; } return suffix.indexOf('@') < 0 ? '@' + suffix : suffix; } @NotNull private static LDAPConnection createConnection(@NotNull SharedContext context, @NotNull LdapUserDBConfig config) throws LDAPException { URI ldapUri = URI.create(config.getConnectionUrl()); SocketFactory factory; int defaultPort; switch (ldapUri.getScheme().toLowerCase()) { case "ldap": factory = null; defaultPort = 389; break; case "ldaps": factory = createSslFactory(context, config); defaultPort = 636; break; default: throw new IllegalStateException("Unknown ldap scheme: " + ldapUri.getScheme()); } int ldapPort = ldapUri.getPort() > 0 ? ldapUri.getPort() : defaultPort; String ldapHost = ldapUri.getHost(); return new LDAPConnection(factory, ldapHost, ldapPort); } @NotNull private static SocketFactory createSslFactory(@NotNull SharedContext context, @NotNull LdapUserDBConfig config) { try { final TrustManager trustManager; final String certPem = config.getLdapCertPem(); if (certPem != null) { final File certFile = ConfigHelper.joinPath(context.getBasePath(), certPem); log.info("Loading CA certificate from: {}", certFile.getAbsolutePath()); trustManager = createTrustManager(Files.readAllBytes(certFile.toPath())); return new SSLUtil(null, trustManager).createSSLSocketFactory(); } else { log.info("CA certificate not defined. Using JVM default SSL context"); return SSLContext.getDefault().getSocketFactory(); } } catch (GeneralSecurityException e) { throw new IllegalStateException(e); } catch (IOException e) { throw new IllegalStateException("Can't load certificate file", e); } } @NotNull public static byte[] parseDERFromPEM(@NotNull byte[] pem, @NotNull String beginDelimiter, @NotNull String endDelimiter) throws GeneralSecurityException { final String data = new String(pem, StandardCharsets.ISO_8859_1); String[] tokens = data.split(beginDelimiter); if (tokens.length != 2) { throw new GeneralSecurityException("Invalid PEM certificate data. Delimiter not found: " + beginDelimiter); } tokens = tokens[1].split(endDelimiter); if (tokens.length != 2) { throw new GeneralSecurityException("Invalid PEM certificate data. Delimiter not found: " + endDelimiter); } return DatatypeConverter.parseBase64Binary(tokens[0]); } @NotNull public static KeyStore getKeyStoreFromDER(@NotNull byte[] certBytes) throws GeneralSecurityException { try { final CertificateFactory factory = CertificateFactory.getInstance("X.509"); final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(null); keystore.setCertificateEntry("alias", factory.generateCertificate(new ByteArrayInputStream(certBytes))); return keystore; } catch (IOException e) { throw new KeyStoreException(e); } } @NotNull public static TrustManager createTrustManager(@NotNull byte[] pem) throws GeneralSecurityException { final TrustManagerFactory factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); final KeyStore keystore = getKeyStoreFromDER(parseDERFromPEM(pem, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----")); factory.init(keystore); final TrustManager[] trustManagers = factory.getTrustManagers(); return new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { for (TrustManager trustManager : trustManagers) { ((X509TrustManager) trustManager).checkClientTrusted(x509Certificates, s); } } @Override public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { for (TrustManager trustManager : trustManagers) { ((X509TrustManager) trustManager).checkServerTrusted(x509Certificates, s); } } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } }; } }