Coverage Summary for Class: LdapServerService (org.kitodo.production.services.data)

Class Class, % Method, % Line, %
LdapServerService 100% (1/1) 18,2% (4/22) 5,3% (15/281)


 /*
  * (c) Kitodo. Key to digital objects e. V. <contact@kitodo.org>
  *
  * This file is part of the Kitodo project.
  *
  * It is licensed under GNU General Public License version 3 or later.
  *
  * For the full copyright and license information, please read the
  * GPL3-License.txt file that was distributed with this source code.
  */
 
 package org.kitodo.production.services.data;
 
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Paths;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
 import java.security.KeyStore;
 import java.security.KeyStoreException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.security.spec.InvalidKeySpecException;
 import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
 import javax.crypto.BadPaddingException;
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
 import javax.naming.Context;
 import javax.naming.NamingEnumeration;
 import javax.naming.NamingException;
 import javax.naming.directory.Attribute;
 import javax.naming.directory.Attributes;
 import javax.naming.directory.BasicAttribute;
 import javax.naming.directory.BasicAttributes;
 import javax.naming.directory.DirContext;
 import javax.naming.directory.InitialDirContext;
 import javax.naming.directory.ModificationItem;
 import javax.naming.directory.SearchResult;
 import javax.naming.ldap.InitialLdapContext;
 import javax.naming.ldap.LdapContext;
 import javax.naming.ldap.StartTlsRequest;
 import javax.naming.ldap.StartTlsResponse;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.bouncycastle.crypto.digests.MD4Digest;
 import org.kitodo.config.ConfigCore;
 import org.kitodo.config.enums.ParameterCore;
 import org.kitodo.data.database.beans.LdapServer;
 import org.kitodo.data.database.beans.User;
 import org.kitodo.data.database.enums.PasswordEncryption;
 import org.kitodo.data.database.exceptions.DAOException;
 import org.kitodo.data.database.persistence.LdapServerDAO;
 import org.kitodo.production.helper.Helper;
 import org.kitodo.production.ldap.LdapUser;
 import org.kitodo.production.security.AESUtil;
 import org.kitodo.production.services.ServiceManager;
 import org.kitodo.production.services.data.base.SearchDatabaseService;
 import org.primefaces.model.SortOrder;
 
 public class LdapServerService extends SearchDatabaseService<LdapServer, LdapServerDAO> {
 
     private static final Logger logger = LogManager.getLogger(LdapServerService.class);
     private static volatile LdapServerService instance = null;
 
     /**
      * Return singleton variable of type LdapServerService.
      *
      * @return unique instance of LdapServerService
      */
     public static LdapServerService getInstance() {
         LdapServerService localReference = instance;
         if (Objects.isNull(localReference)) {
             synchronized (LdapServerService.class) {
                 localReference = instance;
                 if (Objects.isNull(localReference)) {
                     localReference = new LdapServerService();
                     instance = localReference;
                 }
             }
         }
         return localReference;
     }
 
     private LdapServerService() {
         super(new LdapServerDAO());
     }
 
     @Override
     public Long countDatabaseRows() throws DAOException {
         return countDatabaseRows("SELECT COUNT(*) FROM LdapServer");
     }
 
     @Override
     public Long countResults(Map filters) throws DAOException {
         return countDatabaseRows();
     }
 
     @Override
     public List<LdapServer> loadData(int first, int pageSize, String sortField, SortOrder sortOrder, Map filters) {
         return new ArrayList<>();
     }
 
     private String buildUserDN(User inUser) {
         String userDN = inUser.getLdapGroup().getUserDN();
         userDN = userDN.replaceAll("\\{login\\}", inUser.getLogin());
         if (Objects.nonNull(inUser.getLdapLogin())) {
             userDN = userDN.replaceAll("\\{ldaplogin\\}", inUser.getLdapLogin());
         }
         userDN = userDN.replaceAll("\\{firstname\\}", inUser.getName());
         userDN = userDN.replaceAll("\\{lastname\\}", inUser.getSurname());
         return userDN;
     }
 
     private Hashtable<String, String> initializeWithLdapConnectionSettings(LdapServer ldapServer) {
         Hashtable<String, String> env = new Hashtable<>(11);
         env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
         env.put(Context.PROVIDER_URL, ldapServer.getUrl());
         env.put(Context.SECURITY_AUTHENTICATION, "simple");
         env.put(Context.SECURITY_PRINCIPAL, ldapServer.getManagerLogin());
 
         String managerPassword = ldapServer.getManagerPassword();
         if (AESUtil.isEncrypted(managerPassword)) {
             String securitySecret = ConfigCore.getParameterOrDefaultValue(ParameterCore.SECURITY_SECRET_LDAPMANAGERPASSWORD);
 
             if (StringUtils.isBlank(securitySecret)) {
                 logger.error("The security.secret.ldapManagerPassword parameter was not configured in kitodo_config.properties file.");
             }
 
             try {
                 managerPassword = AESUtil.decrypt(managerPassword, securitySecret);
             } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidAlgorithmParameterException
                     | InvalidKeyException | BadPaddingException | IllegalBlockSizeException
                     | InvalidKeySpecException e) {
                 logger.error(e.getLocalizedMessage(), e);
             }
         }
         env.put(Context.SECURITY_CREDENTIALS, managerPassword);
 
         if (ldapServer.isUseSsl()) {
             String keystorepath = ldapServer.getKeystore();
             String keystorepasswd = ldapServer.getKeystorePassword();
 
             // add all necessary certificates first
             loadCertificates(keystorepath, keystorepasswd, ldapServer);
 
             // set properties, so that the current keystore is used for SSL
             System.setProperty("javax.net.ssl.keyStore", keystorepath);
             System.setProperty("javax.net.ssl.trustStore", keystorepath);
             System.setProperty("javax.net.ssl.keyStorePassword", keystorepasswd);
             env.put(Context.SECURITY_PROTOCOL, "ssl");
         }
         return env;
     }
 
     /**
      * create new user in LDAP-directory.
      *
      * @param user
      *            User object
      * @param password
      *            String
      */
     public void createNewUser(User user, String password)
             throws NamingException, NoSuchAlgorithmException, IOException {
 
         if (Objects.isNull(user.getLdapGroup())) {
             Helper.setErrorMessage(Helper.getTranslation("noLdapGroupAssignedToUser"));
             return;
         }
 
         if (Objects.isNull(user.getLdapGroup().getLdapServer())) {
             Helper.setErrorMessage(Helper.getTranslation("noLdapServerAssignedToLdapGroup"));
             return;
         }
 
         if (!user.getLdapGroup().getLdapServer().isReadOnly()) {
             Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(
                 user.getLdapGroup().getLdapServer());
 
             LdapUser ldapUser = new LdapUser();
             ldapUser.configure(user, password, getNextUidNumber(user.getLdapGroup().getLdapServer()));
             DirContext ctx = new InitialDirContext(ldapEnvironment);
             ctx.bind(buildUserDN(user), ldapUser);
             ctx.close();
             setNextUidNumber(user.getLdapGroup().getLdapServer());
             Helper.setMessage(
                 Helper.getTranslation("ldapWritten") + " " + ServiceManager.getUserService().getFullName(user));
             /*
              * check if HomeDir exists, else create it
              */
             logger.debug("HomeVerzeichnis pruefen");
 
             URI homePath = getUserHomeDirectory(user);
 
             if (!new File(homePath).exists()) {
                 logger.debug("HomeVerzeichnis existiert noch nicht");
                 ServiceManager.getFileService().createDirectoryForUser(homePath, user.getLogin());
                 logger.debug("HomeVerzeichnis angelegt");
             } else {
                 logger.debug("HomeVerzeichnis existiert schon");
             }
         } else {
             Helper.setMessage("ldapIsReadOnly");
         }
     }
 
     /**
      * Check if connection with login and password possible.
      *
      * @param user
      *            User object
      * @param password
      *            String
      * @return Login correct or not
      */
     public boolean isUserPasswordCorrect(User user, String password) {
         logger.debug("start login session with ldap");
         Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer());
 
         // Start TLS
         if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_TLS)) {
             logger.debug("use TLS for auth");
             return isPasswordCorrectForAuthWithTLS(env, user, password);
         } else {
             logger.debug("don't use TLS for auth");
             return isPasswordCorrectForAuthWithoutTLS(env, user, password);
         }
     }
 
     /**
      * Retrieve home directory of given user.
      *
      * @param user
      *            User object
      * @return path as URI
      */
     public URI getUserHomeDirectory(User user) {
         String userFolderBasePath = ConfigCore.getParameter(ParameterCore.DIR_USERS);
 
         if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_LOCAL_DIRECTORY)) {
             return Paths.get(userFolderBasePath, user.getLogin()).toUri();
         }
         Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer());
         if (ConfigCore.getBooleanParameterOrDefaultValue(ParameterCore.LDAP_USE_TLS)) {
             return getUserHomeDirectoryWithTLS(env, userFolderBasePath, user);
         }
 
         if (ConfigCore.getBooleanParameter(ParameterCore.LDAP_USE_SIMPLE_AUTH, false)) {
             env.put(Context.SECURITY_AUTHENTICATION, "none");
         }
         DirContext ctx;
         URI userFolderPath = null;
         try {
             ctx = new InitialDirContext(env);
             Attributes attrs = ctx.getAttributes(buildUserDN(user));
             Attribute ldapAttribute = attrs.get("homeDirectory");
             userFolderPath = URI.create((String) ldapAttribute.get(0));
             ctx.close();
         } catch (NamingException e) {
             logger.error(e.getMessage(), e);
         }
 
         if (Objects.nonNull(userFolderPath) && !userFolderPath.isAbsolute()) {
             if (userFolderPath.getPath().startsWith("/")) {
                 userFolderPath = ServiceManager.getFileService().deleteFirstSlashFromPath(userFolderPath);
             }
             return Paths.get(userFolderBasePath, userFolderPath.getRawPath()).toUri();
         } else {
             return userFolderPath;
         }
     }
 
     /**
      * Check if User already exists on system.
      *
      * @param user
      *            The User.
      * @return whether the user already exists
      */
     public boolean isUserAlreadyExists(User user) {
         Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(
             user.getLdapGroup().getLdapServer());
         DirContext ctx;
         boolean userAlreadyExisting = false;
         try {
             ctx = new InitialDirContext(ldapEnvironment);
             Attributes matchAttrs = new BasicAttributes(true);
             NamingEnumeration<SearchResult> answer = ctx.search(buildUserDN(user), matchAttrs);
             userAlreadyExisting = answer.hasMoreElements();
 
             while (answer.hasMore()) {
                 SearchResult sr = answer.next();
                 logger.debug(">>>{}", sr.getName());
                 Attributes attrs = sr.getAttributes();
                 String givenName = getStringForAttribute(attrs, "givenName");
                 String surName = getStringForAttribute(attrs, "sn");
                 String mail = getStringForAttribute(attrs, "mail");
                 String cn = getStringForAttribute(attrs, "cn");
                 String homeDirectory = getStringForAttribute(attrs, "homeDirectory");
 
                 logger.debug(givenName);
                 logger.debug(surName);
                 logger.debug(mail);
                 logger.debug(cn);
                 logger.debug(homeDirectory);
             }
 
             ctx.close();
         } catch (NamingException e) {
             logger.error(e.getMessage(), e);
         }
         return userAlreadyExisting;
     }
 
     private String getStringForAttribute(Attributes attrs, String identifier) {
         try {
             return attrs.get(identifier).toString();
         } catch (RuntimeException e) {
             return " ";
         }
     }
 
     /**
      * Get next free uidNumber.
      *
      * @return next free uidNumber
      */
     private String getNextUidNumber(LdapServer ldapServer) {
         Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(ldapServer);
         DirContext ctx;
         String rueckgabe = "";
         try {
             ctx = new InitialDirContext(ldapEnvironment);
             Attributes attrs = ctx.getAttributes(ldapServer.getNextFreeUnixIdPattern());
             Attribute la = attrs.get("uidNumber");
             rueckgabe = (String) la.get(0);
             ctx.close();
         } catch (NamingException e) {
             Helper.setErrorMessage(e.getLocalizedMessage(), logger, e);
         }
         return rueckgabe;
     }
 
     /**
      * Set next free uidNumber.
      */
     private void setNextUidNumber(LdapServer ldapServer) {
         Hashtable<String, String> ldapEnvironment = initializeWithLdapConnectionSettings(ldapServer);
         DirContext ctx;
 
         try {
             ctx = new InitialDirContext(ldapEnvironment);
             Attributes attrs = ctx.getAttributes(ldapServer.getNextFreeUnixIdPattern());
             Attribute la = attrs.get("uidNumber");
             String oldValue = (String) la.get(0);
             int bla = Integer.parseInt(oldValue) + 1;
 
             BasicAttribute attrNeu = new BasicAttribute("uidNumber", String.valueOf(bla));
             ModificationItem[] mods = new ModificationItem[1];
             mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attrNeu);
             ctx.modifyAttributes(ldapServer.getNextFreeUnixIdPattern(), mods);
 
             ctx.close();
         } catch (NamingException e) {
             logger.error(e.getMessage(), e);
         }
 
     }
 
     /**
      * change password of given user, needs old password for authentication.
      *
      * @param user
      *            User object
      * @param inNewPassword
      *            String
      * @return boolean about result of change
      */
     public boolean changeUserPassword(User user, String inNewPassword) throws NoSuchAlgorithmException {
         MD4Digest digester = new MD4Digest();
         PasswordEncryption passwordEncryption = user.getLdapGroup().getLdapServer().getPasswordEncryption();
         Hashtable<String, String> env = initializeWithLdapConnectionSettings(user.getLdapGroup().getLdapServer());
         if (!user.getLdapGroup().getLdapServer().isReadOnly()) {
             try {
                 ModificationItem[] mods = new ModificationItem[4];
 
                 // encryption of password and Base64-Encoding
                 MessageDigest md = MessageDigest.getInstance(passwordEncryption.getTitle());
                 md.update(inNewPassword.getBytes(StandardCharsets.UTF_8));
                 String encryptedPassword = new String(Base64.encodeBase64(md.digest()), StandardCharsets.UTF_8);
 
                 // change attribute userPassword
                 BasicAttribute userPassword = new BasicAttribute("userPassword",
                         "{" + passwordEncryption + "}" + encryptedPassword);
                 mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, userPassword);
 
                 // change attribute lanmgrPassword
                 BasicAttribute lanmgrPassword = proceedPassword("sambaLMPassword", inNewPassword, null);
                 mods[1] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, lanmgrPassword);
 
                 // change attribute ntlmPassword
                 BasicAttribute ntlmPassword = proceedPassword("sambaNTPassword", inNewPassword, digester);
                 mods[2] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, ntlmPassword);
 
                 BasicAttribute sambaPwdLastSet = new BasicAttribute("sambaPwdLastSet",
                         String.valueOf(System.currentTimeMillis() / 1000L));
                 mods[3] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, sambaPwdLastSet);
 
                 DirContext ctx = new InitialDirContext(env);
                 ctx.modifyAttributes(buildUserDN(user), mods);
 
                 // Close the context when we're done
                 ctx.close();
                 return true;
             } catch (NamingException e) {
                 logger.debug("Benutzeranmeldung nicht korrekt oder Passwortänderung nicht möglich", e);
                 return false;
             }
         }
         return false;
     }
 
     private URI getUserHomeDirectoryWithTLS(Hashtable<String, String> env, String userFolderBasePath, User user) {
         env.put("java.naming.ldap.version", "3");
         LdapContext ctx = null;
         StartTlsResponse tls = null;
         try {
             ctx = new InitialLdapContext(env, null);
 
             // Authentication must be performed over a secure channel
             tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
             tls.negotiate();
 
             ctx.reconnect(null);
 
             Attributes attrs = ctx.getAttributes(buildUserDN(user));
             Attribute la = attrs.get("homeDirectory");
             return URI.create((String) la.get(0));
         } catch (IOException e) {
             logger.error("TLS negotiation error:", e);
             return Paths.get(userFolderBasePath, user.getLogin()).toUri();
         } catch (NamingException e) {
             logger.error("JNDI error:", e);
             return Paths.get(userFolderBasePath, user.getLogin()).toUri();
         } finally {
             closeConnections(ctx, tls);
         }
     }
 
     private boolean isPasswordCorrectForAuthWithTLS(Hashtable<String, String> env, User user, String password) {
         env.put("java.naming.ldap.version", "3");
         LdapContext ctx = null;
         StartTlsResponse tls = null;
         try {
             ctx = new InitialLdapContext(env, null);
 
             // Authentication must be performed over a secure channel
             tls = (StartTlsResponse) ctx.extendedOperation(new StartTlsRequest());
             tls.negotiate();
 
             // Authenticate via SASL EXTERNAL mechanism using client X.509
             // certificate contained in JVM keystore
             ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");
             ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, buildUserDN(user));
             ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, password);
             ctx.reconnect(null);
             return true;
             // perform search for privileged attributes under authenticated context
         } catch (IOException e) {
             logger.error("TLS negotiation error:", e);
             return false;
         } catch (NamingException e) {
             logger.error("JNDI error:", e);
             return false;
         } finally {
             closeConnections(ctx, tls);
         }
     }
 
     private boolean isPasswordCorrectForAuthWithoutTLS(Hashtable<String, String> env, User user, String password) {
         if (ConfigCore.getBooleanParameter(ParameterCore.LDAP_USE_SIMPLE_AUTH, false)) {
             env.put(Context.SECURITY_AUTHENTICATION, "none");
             // TODO: test for password
         } else {
             env.put(Context.SECURITY_PRINCIPAL, buildUserDN(user));
             env.put(Context.SECURITY_CREDENTIALS, password);
         }
         logger.debug("ldap environment set");
 
         try {
             logger.debug("start classic ldap authentication");
             logger.debug("user DN is {}", buildUserDN(user));
 
             if (Objects.isNull(ConfigCore.getParameter(ParameterCore.LDAP_ATTRIBUTE_TO_TEST))) {
                 logger.debug("ldap attribute to test is null");
                 DirContext ctx = new InitialDirContext(env);
                 ctx.close();
                 return true;
             } else {
                 logger.debug("ldap attribute to test is not null");
                 DirContext ctx = new InitialDirContext(env);
 
                 Attributes attrs = ctx.getAttributes(buildUserDN(user));
                 Attribute la = attrs.get(ConfigCore.getParameter(ParameterCore.LDAP_ATTRIBUTE_TO_TEST));
                 logger.debug("ldap attributes set");
                 String test = (String) la.get(0);
                 if (test.equals(ConfigCore.getParameter(ParameterCore.LDAP_VALUE_OF_ATTRIBUTE))) {
                     logger.debug("ldap ok");
                     ctx.close();
                     return true;
                 } else {
                     logger.debug("ldap not ok");
                     ctx.close();
                     return false;
                 }
             }
         } catch (NamingException e) {
             logger.debug("login not allowed for {}. Exception: {}", user.getLogin(), e);
             return false;
         }
     }
 
     private void closeConnections(LdapContext ctx, StartTlsResponse tls) {
         if (Objects.nonNull(tls)) {
             try {
                 // Tear down TLS connection
                 tls.close();
             } catch (IOException e) {
                 logger.error(e.getMessage(), e);
             }
         }
         if (Objects.nonNull(ctx)) {
             try {
                 // Close LDAP connection
                 ctx.close();
             } catch (NamingException e) {
                 logger.error(e.getMessage(), e);
             }
         }
     }
 
     private BasicAttribute proceedPassword(String identifier, String newPassword, MD4Digest digester) {
         try {
             byte[] hash;
             if (Objects.isNull(digester)) {
                 hash = LdapUser.lmHash(newPassword);
             } else {
                 byte[] unicodePassword = newPassword.getBytes(StandardCharsets.UTF_16LE);
                 hash = new byte[digester.getDigestSize()];
                 digester.update(unicodePassword, 0, unicodePassword.length);
                 digester.doFinal(hash, 0);
             }
             return new BasicAttribute(identifier, LdapUser.toHexString(hash));
             // TODO: Don't catch super class exception, make sure that
             // the password isn't logged here
         } catch (InvalidKeyException | NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException
                 | BadPaddingException | RuntimeException e) {
             logger.error(e.getMessage(), e);
             return null;
         }
     }
 
     // TODO test if this methods works
     private void loadCertificates(String path, String passwd, LdapServer ldapServer) {
         /* wenn die Zertifikate noch nicht im Keystore sind, jetzt einlesen */
         File myPfad = new File(path);
         if (!myPfad.exists()) {
             try (FileOutputStream ksos = (FileOutputStream) ServiceManager.getFileService().write(myPfad.toURI());
                     // TODO: Rename parameters to something more meaningful,
                     // this is quite specific for the GDZ
                     FileInputStream cacertFile = new FileInputStream(ldapServer.getRootCertificate());
                     FileInputStream certFile2 = new FileInputStream(ldapServer.getPdcCertificate())) {
 
                 CertificateFactory cf = CertificateFactory.getInstance("X.509");
                 X509Certificate cacert = (X509Certificate) cf.generateCertificate(cacertFile);
                 X509Certificate servercert = (X509Certificate) cf.generateCertificate(certFile2);
 
                 KeyStore ks = KeyStore.getInstance("jks");
                 char[] password = passwd.toCharArray();
 
                 // TODO: Let this method really load a keystore if configured
                 // initialize the keystore, if file is available, load the
                 // keystore
                 ks.load(null);
 
                 ks.setCertificateEntry("ROOTCERT", cacert);
                 ks.setCertificateEntry("PDC", servercert);
                 ks.store(ksos, password);
             } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException
                     | RuntimeException e) {
                 logger.error(e.getMessage(), e);
             }
 
         }
     }
 }