/* * Copyright 2015 ThoughtWorks, Inc. * * 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 com.thoughtworks.go.server.security; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttributes; import javax.naming.directory.SearchControls; import com.thoughtworks.go.config.LdapConfig; import com.thoughtworks.go.config.SecurityConfig; import com.thoughtworks.go.config.server.security.ldap.BaseConfig; import com.thoughtworks.go.domain.User; import com.thoughtworks.go.server.service.GoConfigService; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.AttributesMapperCallbackHandler; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.DirContextOperations; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.filter.LikeFilter; import org.springframework.ldap.filter.OrFilter; import org.springframework.security.BadCredentialsException; import org.springframework.security.ldap.SpringSecurityContextSource; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @Service public class LdapUserSearch implements org.springframework.security.ldap.LdapUserSearch { private final GoConfigService goConfigService; private final Logger logger; private final SpringSecurityContextSource contextFactory; private static final String SAM_ACCOUNT_NAME = "sAMAccountName"; private static final String UID = "uid"; private static final String COMMON_NAME = "cn"; private static final String MAIL_ID = "mail"; private static final String ALIAS_EMAIL_ID = "otherMailbox"; private static final long MAX_RESULTS = 100; private LdapTemplate ldapTemplate; @Autowired public LdapUserSearch(GoConfigService goConfigService, ContextSource contextFactory) { this(goConfigService, contextFactory, new LdapTemplate(contextFactory), Logger.getLogger(LdapUserSearch.class)); } public LdapUserSearch(GoConfigService goConfigService, ContextSource contextFactory, final LdapTemplate ldapTemplate, Logger logger) { this.goConfigService = goConfigService; this.logger = logger; this.contextFactory = (SpringSecurityContextSource) contextFactory; this.ldapTemplate = ldapTemplate; } public DirContextOperations searchForUser(String username) { SecurityConfig securityConfig = goConfigService.security(); if (!securityConfig.isSecurityEnabled()) { return null; } LdapConfig ldapConfig = securityConfig.ldapConfig(); RuntimeException lastFoundException = null; BaseConfig failedBaseConfig = null; for (BaseConfig baseConfig : ldapConfig.getBasesConfig()) { if(lastFoundException != null && !(lastFoundException instanceof BadCredentialsException)) { logger.warn(String.format("The ldap configuration for search base '%s' is invalid", failedBaseConfig.getValue()),lastFoundException); } FilterBasedLdapUserSearch search = getFilterBasedLdapUserSearch(baseConfig.getValue(), ldapConfig.searchFilter()); search.setSearchSubtree(true); search.setSearchTimeLimit(5000); // timeout after five seconds try { return search.searchForUser(username); } catch (UsernameNotFoundException e) { failedBaseConfig = baseConfig; lastFoundException = new BadCredentialsException("Bad credentials"); } catch (RuntimeException e) { failedBaseConfig = baseConfig; lastFoundException = e; } } if(lastFoundException != null) { throw lastFoundException; } throw new RuntimeException("No LDAP Search Bases are configured."); } public List<User> search(String username) { SecurityConfig securityConfig = goConfigService.security(); return search(username, securityConfig.ldapConfig()); } public List<User> search(String username, LdapConfig ldapConfig) { if(ldapConfig.getBasesConfig().isEmpty()) { throw new RuntimeException("Atleast one Search Base needs to be configured."); } OrFilter filter = new OrFilter(); String searchString = MessageFormat.format("*{0}*", username); filter.or(new LikeFilter(SAM_ACCOUNT_NAME, searchString)); filter.or(new LikeFilter(UID, searchString)); filter.or(new LikeFilter(COMMON_NAME, searchString)); filter.or(new LikeFilter(MAIL_ID, searchString)); filter.or(new LikeFilter(ALIAS_EMAIL_ID, searchString)); // This field is optional to search based on. Only for alias emails. //List ldapUserList = template.search(ldapConfig.searchBase(), filter.encode(), attributes); SearchControls controls = new SearchControls(); controls.setSearchScope(SearchControls.SUBTREE_SCOPE); controls.setCountLimit(MAX_RESULTS); AttributesMapperCallbackHandler handler = getAttributesMapperCallbackHandler(); for (BaseConfig baseConfig : ldapConfig.getBasesConfig()) { try { ldapTemplate.search(baseConfig.getValue(), filter.encode(), controls, handler); } catch (org.springframework.ldap.LimitExceededException e) { throw new NotAllResultsShownException(buildUserList(handler.getList())); } } return buildUserList(handler.getList()); } AttributesMapperCallbackHandler getAttributesMapperCallbackHandler() { AttributesMapper attributes = new AttributesMapper() { public Object mapFromAttributes(Attributes attributes) throws NamingException { return attributes; } }; return new AttributesMapperCallbackHandler(attributes); } private List<User> buildUserList(List ldapUserList) { List<User> users = new ArrayList<>(); for (Object ldapUser : ldapUserList) { try { users.add(toUser((BasicAttributes) ldapUser)); } catch (NamingException e) { throw new RuntimeException("Ldap attributes configured incorrectly. Mismatch in expected attributes.", e); } } return users; } private User toUser(BasicAttributes attributes) throws NamingException { String fullName = attributes.get(COMMON_NAME).get().toString(); Attribute samAccName = attributes.get(SAM_ACCOUNT_NAME); String loginName; loginName = samAccName != null ? samAccName.get().toString() : attributes.get(UID).get().toString(); Attribute emailAttr = attributes.get(MAIL_ID); String emailAddress = emailAttr != null ? emailAttr.get().toString() : ""; return new User(loginName, fullName, emailAddress); } public FilterBasedLdapUserSearch getFilterBasedLdapUserSearch(final String searchBase, final String searchFilter) { return new FilterBasedLdapUserSearch(searchBase, searchFilter, contextFactory); } public static class NotAllResultsShownException extends RuntimeException { private final List<User> users; public NotAllResultsShownException(List<User> users) { this.users = users; } public List<User> getUsers() { return users; } } }