/* See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * Esri Inc. licenses this file to You 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 com.esri.gpt.framework.security.identity.ldap; import com.esri.gpt.framework.collection.StringSet; import com.esri.gpt.framework.context.RequestContext; import com.esri.gpt.framework.security.credentials.Credentials; import com.esri.gpt.framework.security.credentials.CredentialsDeniedException; import com.esri.gpt.framework.security.credentials.DistinguishedNameCredential; import com.esri.gpt.framework.security.credentials.UsernameCredential; import com.esri.gpt.framework.security.credentials.UsernamePasswordCredentials; import com.esri.gpt.framework.security.identity.IdentityException; import com.esri.gpt.framework.security.identity.local.LocalDao; import com.esri.gpt.framework.security.principal.Group; import com.esri.gpt.framework.security.principal.Groups; import com.esri.gpt.framework.security.principal.Role; import com.esri.gpt.framework.security.principal.RoleSet; import com.esri.gpt.framework.security.principal.Roles; import com.esri.gpt.framework.security.principal.User; import com.esri.gpt.framework.util.LogUtil; import com.esri.gpt.framework.util.Val; import java.sql.SQLException; import java.util.*; import java.util.logging.Level; import javax.naming.AuthenticationException; import javax.naming.Context; import javax.naming.directory.*; import javax.naming.ldap.InitialLdapContext; import javax.naming.NamingException; /** * A client for connection to an LDAP identity store. */ public class LdapClient { // class variables ============================================================= // instance variables ========================================================== private LdapConfiguration _configuration = null; private Credentials _credentials = null; private DirContext _dirContext = null; private LdapEditFunctions _editFunctions = new LdapEditFunctions(); private LdapQueryFunctions _queryFunctions = new LdapQueryFunctions(); // constructors ================================================================ /** Default constructor. */ protected LdapClient() { this(null,null); } /** * Construct with a supplied configuration. * @param configuration the configuration */ protected LdapClient(LdapConfiguration configuration) { this(configuration,null); } /** * Construct with a supplied configuration and credentials. * @param configuration the configuration * @param credentials the connection credentials */ protected LdapClient(LdapConfiguration configuration, Credentials credentials) { if (configuration == null) { setConfiguration(new LdapConfiguration()); } else { setConfiguration(configuration); } if (credentials == null) { setCredentials(configuration.getConnectionProperties().getServiceAccountCredentials()); } else { setCredentials(credentials); } } // properties ================================================================== /** * Gets the LDAP configuration. * @return the configuration */ public LdapConfiguration getConfiguration() { return _configuration; } /** * Sets the LDAP configuration. * @param configuration the configuration */ public void setConfiguration(LdapConfiguration configuration) { _configuration = configuration; getEditFunctions().setConfiguration(configuration); getQueryFunctions().setConfiguration(configuration); } /** * Gets the connected directory context. * @return the connected directory context * @throws NamingException if a connection has not been established */ protected final DirContext getConnectedContext() throws NamingException { if (_dirContext == null) { throw new NamingException("An LDAP connection has not been established."); } return _dirContext; } /** * Sets the connected directory context. * @param connectedContext the connected directory context */ protected final void setConnectedContext(DirContext connectedContext) { // ensure that the current connection context is closed before resetting try { if (_dirContext != null) { _dirContext.close(); _dirContext = null; } } catch (Exception e) { LogUtil.getLogger().log(Level.WARNING,"Error closing LDAP directory context.",e); } _dirContext = connectedContext; } /** * Gets the credentials for the connection. * @return the credentials */ public Credentials getCredentials() { return _credentials; } /** * Sets the credentials for the connection. * @param credentials the credentials */ public void setCredentials(Credentials credentials) { _credentials = credentials; } /** * Gets the edit functions. * @return the edit functions */ protected LdapEditFunctions getEditFunctions() { return _editFunctions; } /** * Gets the query functions. * @return the query functions */ protected LdapQueryFunctions getQueryFunctions() { return _queryFunctions; } // methods ===================================================================== /** * Authenticates a user. * @param requestContext the context associated with the request * @param user the subject user * @throws CredentialsDeniedException if credentials are denied * @throws IdentityException if a system error occurs preventing authentication * @throws SQLException if a database communication exception occurs */ protected void authenticate(RequestContext requestContext, User user) throws CredentialsDeniedException, IdentityException, SQLException { LdapClient connectionClient = null; try { user.getAuthenticationStatus().reset(); String sUsername = ""; String sAuthenticatedDN = ""; String sTargetedGroupDN = ""; LdapUserProperties userProps = getConfiguration().getUserProperties(); // determine the authentication method Credentials credentials = user.getCredentials(); UsernamePasswordCredentials upCredentials = null; boolean bUseDirectConnect = false; boolean bUseLoginPattern = false; if (credentials != null) { if (credentials instanceof UsernamePasswordCredentials) { upCredentials = (UsernamePasswordCredentials)credentials; upCredentials.setTargetedGroupDN(""); sUsername = upCredentials.getUsername(); String sPattern = userProps.getUsernameSearchPattern(); if (sUsername.length() > 0) { if (userProps.hasSpecialDNCharacter(sUsername)) { bUseDirectConnect = true; } else { bUseLoginPattern = (sPattern.length() > 0); } } } else if (credentials instanceof DistinguishedNameCredential) { DistinguishedNameCredential dnCredential; dnCredential = (DistinguishedNameCredential)credentials; sAuthenticatedDN = dnCredential.getDistinguishedName(); } else if (credentials instanceof UsernameCredential) { UsernameCredential unCredential = (UsernameCredential)credentials; String sBaseDN = userProps.getUserSearchDIT(); String sFilter = userProps.returnUserLoginSearchFilter(unCredential.getUsername()); StringSet ssDNs = getQueryFunctions().searchDNs( getConnectedContext(),sBaseDN,sFilter); if (ssDNs.size() > 1) { throw new IdentityException("Multiple LDAP usernames matched for:"+ unCredential.getUsername()); } else if (ssDNs.size() == 1) { sAuthenticatedDN = ssDNs.iterator().next(); } } } // Attempt to connect with the supplied credentials. // An AuthenticationException will be thrown if the credentials are invalid if (bUseDirectConnect) { connectionClient = new LdapClient(getConfiguration(),upCredentials); sAuthenticatedDN = connectionClient.connect(); bUseLoginPattern = false; connectionClient.close(); connectionClient = null; } // Attempt to authenticate by first executing a search for all users // matching the input username, then checking the supplied password against // each matching DN. // An AuthenticationException will be thrown if the credentials are invalid. if (bUseLoginPattern) { sAuthenticatedDN = searchForUser(upCredentials); sTargetedGroupDN = upCredentials.getTargetedGroupDN(); } // ensure an authenticated DN if (sAuthenticatedDN.length() == 0) { throw new AuthenticationException("Invalid credentials."); } // populate the authentication status and profile information user.setDistinguishedName(sAuthenticatedDN); populateUser(requestContext,user,sTargetedGroupDN); RoleSet roles = user.getAuthenticationStatus().getAuthenticatedRoles(); if (roles.hasRole("gptForbiddenAccess")) { User activeUser = requestContext.getUser(); if(activeUser.getAuthenticationStatus().getWasAuthenticated()){ String activeUserDn = requestContext.getUser().getDistinguishedName(); String managedUserDn = user.getDistinguishedName(); if(activeUserDn.equals(managedUserDn)){ throw new AuthenticationException("Forbidden"); } }else{ throw new AuthenticationException("Forbidden"); } } } catch (AuthenticationException e) { user.getAuthenticationStatus().reset(); throw new CredentialsDeniedException("Invalid credentials."); } catch (com.esri.gpt.framework.context.ConfigurationException e) { user.getAuthenticationStatus().reset(); throw new IdentityException(e.getMessage(),e); } catch (NamingException e) { user.getAuthenticationStatus().reset(); throw new IdentityException(e.getMessage(),e); } catch (SQLException e) { user.getAuthenticationStatus().reset(); throw e; } catch (IdentityException e) { user.getAuthenticationStatus().reset(); throw e; } finally { if (connectionClient != null) connectionClient.close(); } } /** * Checks the distinguished name within a set of username/password credentials. * <br/>If the distinguished name has not been set, the configured * username pattern is applied to determine the distinguished name. * @param credentials the credentials to check */ private void checkDistinguishedName(UsernamePasswordCredentials credentials) { String sDN = credentials.getDistinguishedName(); if (sDN.length() == 0) { credentials.setDistinguishedName(credentials.getUsername()); } } /** * Closes the connected directory context (if open). */ public final void close() { setConnectedContext(null); } /** * Establishes an LDAP connection. * @return the SECURITY_PRINCIPAL associated with the connection * @throws AuthenticationException if an authentication exception occurs * @throws NamingException if a naming exception occurs */ protected String connect() throws AuthenticationException, NamingException { close(); LdapConfiguration configuration = getConfiguration(); LdapConnectionProperties conProps = configuration.getConnectionProperties(); boolean bForceCredentials = true; String sAuthenticationLevel = conProps.getSecurityAuthenticationLevel(); String sSecurityProtocol = conProps.getSecurityProtocol(); String sPrincipal = ""; String sPassword = ""; // check the credentials Credentials credentials = getCredentials(); if (credentials != null) { if (credentials instanceof UsernamePasswordCredentials) { UsernamePasswordCredentials upCredentials = (UsernamePasswordCredentials)credentials; checkDistinguishedName(upCredentials); sPrincipal = upCredentials.getDistinguishedName(); sPassword = upCredentials.getPassword(); } } // make the environment map Hashtable<String,String> env = new Hashtable<String,String>(11); env.put(Context.INITIAL_CONTEXT_FACTORY,conProps.getInitialContextFactoryName()); env.put(Context.PROVIDER_URL,conProps.getProviderUrl()); if (sAuthenticationLevel.length() > 0) { env.put(Context.SECURITY_AUTHENTICATION,sAuthenticationLevel); } if (sSecurityProtocol.length() > 0) { env.put(Context.SECURITY_PROTOCOL,sSecurityProtocol); } if (sPrincipal.length() > 0) { env.put(Context.SECURITY_PRINCIPAL,sPrincipal); } else if (bForceCredentials) { throw new AuthenticationException("Invalid credentials."); } if (sPassword.length() > 0) { env.put(Context.SECURITY_CREDENTIALS,sPassword); } else if (bForceCredentials) { throw new AuthenticationException("Invalid credentials."); } // env.put(Context.REFERRAL,"follow"); // make the initial directory context boolean useInitialLdapContext = false; LdapGroupProperties groupProps = configuration.getGroupProperties(); String sDyn1 = Val.chkStr(groupProps.getGroupDynamicMemberAttribute()); String sDyn2 = Val.chkStr(groupProps.getGroupDynamicMembersAttribute()); if (sDyn1.startsWith("controlid=") || sDyn2.startsWith("controlid=")) { useInitialLdapContext = true; } if (useInitialLdapContext) { setConnectedContext(new InitialLdapContext(env,null)); } else { setConnectedContext(new InitialDirContext(env)); } return sPrincipal; } /** * Finalize on garbage collection. * @throws Throwable if an exception occurs */ @Override protected void finalize() throws Throwable { super.finalize(); close(); } /** * Populates the authentication status and profile information for * a user based upon the user's DN. * @param requestContext the context associated with the request * @param user the subject user * @throws IdentityException if a system error occurs preventing authentication * @throws NamingException if an LDAP naming exception occurs * @throws SQLException if a database communication exception occurs */ protected void populateUser(RequestContext requestContext, User user, String targetedGroupDN) throws IdentityException, NamingException, SQLException { // initialize String sAuthenticatedDN = user.getDistinguishedName(); user.getAuthenticationStatus().reset(); DirContext dirContext = getConnectedContext(); // ensure an authenticated DN if (sAuthenticatedDN.length() == 0) { throw new AuthenticationException("Invalid credentials."); } // populate profile information user.setDistinguishedName(sAuthenticatedDN); user.setKey(user.getDistinguishedName()); getQueryFunctions().readUserProfile(dirContext,user); user.setName(user.getProfile().getUsername()); // read groups, set authenticated roles getQueryFunctions().readUserGroups(dirContext,user); Groups userGroups = user.getGroups(); Roles configuredRoles = getConfiguration().getIdentityConfiguration().getConfiguredRoles(); RoleSet authenticatedRoles = user.getAuthenticationStatus().getAuthenticatedRoles(); for (Role role: configuredRoles.values()) { if (userGroups.containsKey(role.getDistinguishedName())) { authenticatedRoles.addAll(role.getFullRoleSet()); } } user.getAuthenticationStatus().setWasAuthenticated(true); // ensure membership if a targeted metadata management group was specified if (targetedGroupDN.length() > 0) { if (!userGroups.containsKey(targetedGroupDN)) { user.getAuthenticationStatus().reset(); throw new AuthenticationException("Invalid credentials, not a member of the supplied group."); } } // ensure a local reference for the user LocalDao localDao = new LocalDao(requestContext); localDao.ensureReferenceToRemoteUser(user); } /** * Searches for a user by first executing a search for all users matching the * supplied username credential, then checking the supplied password credential * against each matching DN. * @param credentials the credentials to authenticate * @return the distinguised name associated with a located user * @throws AuthenticationException if the authentication of credentials failed * @throws NamingException if an LDAP naming exception occurs */ protected String searchForUser(UsernamePasswordCredentials credentials) throws AuthenticationException, NamingException { LdapClient client = null; String sAuthenticatedDN = ""; boolean bMultipleAuthenticated = false; try { String sUsername = credentials.getUsername(); // check for a metadata management login: username@@group int nIdx = sUsername.indexOf("@@"); if (nIdx != -1) { Groups mmGroups = getConfiguration().getIdentityConfiguration().getMetadataManagementGroups(); if ((mmGroups != null) && (mmGroups.size() > 0)) { String sMmUser = Val.chkStr(sUsername.substring(0,nIdx)); String sMmGroup = Val.chkStr(sUsername.substring(nIdx+2)); if ((sMmUser.length() > 0) && (sMmGroup.length() > 0)) { for (Group group: mmGroups.values()) { if (sMmGroup.equalsIgnoreCase(group.getName())) { sUsername = sMmUser; credentials.setTargetedGroupDN(group.getDistinguishedName()); } } } } } // search for the user LdapUserProperties userProps = getConfiguration().getUserProperties(); String sBaseDN = userProps.getUserSearchDIT(); String sFilter = userProps.returnUserLoginSearchFilter(sUsername); StringSet ssDNs = getQueryFunctions().searchDNs( getConnectedContext(),sBaseDN,sFilter); // loop through each DN found, // attempt to connect with the supplied password for (String sDN: ssDNs) { credentials.setDistinguishedName(sDN); client = new LdapClient(getConfiguration(),credentials); try { String sTestDN = client.connect(); client.close(); if (sAuthenticatedDN.length() == 0) { sAuthenticatedDN = sTestDN; } else { sAuthenticatedDN = ""; bMultipleAuthenticated = true; break; } } catch (AuthenticationException e) { client.close(); } } // throw an exception if authentication failed if (bMultipleAuthenticated) { // more than one username/password match was found String sMsg = "Multiple LDAP credential matches were found for login: "+sUsername; LogUtil.getLogger().warning(sMsg); throw new AuthenticationException(sMsg); } else if (sAuthenticatedDN.length() == 0) { // no username/password match was found throw new AuthenticationException("Invalid credentials."); } } finally { credentials.setDistinguishedName(sAuthenticatedDN); if (client != null) client.close(); } return sAuthenticatedDN; } }