/* * 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 com.thoughtworks.go.config.LdapConfig; import com.thoughtworks.go.config.SecurityConfig; import com.thoughtworks.go.config.server.security.ldap.BaseConfig; import com.thoughtworks.go.config.server.security.ldap.BasesConfig; import com.thoughtworks.go.server.service.GoConfigService; import org.apache.log4j.Logger; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.Matchers; 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.security.BadCredentialsException; import org.springframework.security.ldap.SpringSecurityContextSource; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.userdetails.UsernameNotFoundException; import javax.naming.directory.SearchControls; import java.util.Arrays; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.*; public class LdapUserSearchTest { private GoConfigService goConfigService; private ContextSource contextFactory; private LdapUserSearch ldapUserSearch; private SecurityConfig securityConfig; private LdapTemplate ldapTemplate; private Logger logger; private LdapUserSearch spy; @Rule public ExpectedException thrown = ExpectedException.none(); @Before public void setUp() { goConfigService = mock(GoConfigService.class); contextFactory = mock(SpringSecurityContextSource.class); securityConfig = mock(SecurityConfig.class); ldapTemplate = mock(LdapTemplate.class); logger = mock(Logger.class); ldapUserSearch = new LdapUserSearch(goConfigService, contextFactory, ldapTemplate, logger); when(goConfigService.security()).thenReturn(securityConfig); spy = spy(ldapUserSearch); } @Test public void shouldThrowBadCredentialsExceptionWhenNoUserFound_WithOneSearchBase() { final FilterBasedLdapUserSearch filterBasedLdapUserSearch = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("search_base,foo"))); doReturn(filterBasedLdapUserSearch).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().first().getValue(), ldapConfig.searchFilter()); when(filterBasedLdapUserSearch.searchForUser("username")).thenThrow(new UsernameNotFoundException("User username not found in directory.")); thrown.expect(BadCredentialsException.class); thrown.expectMessage(is("Bad credentials")); spy.searchForUser("username"); verify(filterBasedLdapUserSearch).searchForUser("username"); } @Test public void shouldThrowBadCredentialsExceptionWhenNoUserFound_WithMultipleSearchBase() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); when(filter1.searchForUser("username")).thenThrow(new UsernameNotFoundException("User username not found in directory.")); when(filter2.searchForUser("username")).thenThrow(new UsernameNotFoundException("User username not found in directory.")); thrown.expect(BadCredentialsException.class); thrown.expectMessage(is("Bad credentials")); spy.searchForUser("username"); verify(filter1).searchForUser("username"); verify(filter2).searchForUser("username"); } @Test public void shouldReturnUserFoundInSecondSearchBase() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); when(filter1.searchForUser("username")).thenThrow(new UsernameNotFoundException("User username not found in directory.")); DirContextOperations foundUser = mock(DirContextOperations.class); when(filter2.searchForUser("username")).thenReturn(foundUser); assertThat(spy.searchForUser("username"), is(foundUser)); verify(filter1).searchForUser("username"); verify(filter2).searchForUser("username"); } @Test public void shouldNotProceedWithTheNextSearchBaseWhenUserIsFoundInOne() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); DirContextOperations foundUser = mock(DirContextOperations.class); when(filter1.searchForUser("username")).thenReturn(foundUser); assertThat(spy.searchForUser("username"), is(foundUser)); verify(filter1).searchForUser("username"); verify(filter2, never()).searchForUser("username"); } @Test public void shouldSkipInvalidSearchBaseWhenAuthenticating() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); DirContextOperations foundUser = mock(DirContextOperations.class); when(filter1.searchForUser("username")).thenThrow(new RuntimeException("Invalid search base")); when(filter2.searchForUser("username")).thenReturn(foundUser); assertThat(spy.searchForUser("username"), is(foundUser)); verify(filter1).searchForUser("username"); verify(filter2).searchForUser("username"); } @Test public void shouldLogErrorsDueToInvalidSearchBaseWhenAuthenticating() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); DirContextOperations foundUser = mock(DirContextOperations.class); RuntimeException runtimeException = new RuntimeException("Invalid search base"); when(filter1.searchForUser("username")).thenThrow(runtimeException); when(filter2.searchForUser("username")).thenReturn(foundUser); assertThat(spy.searchForUser("username"), is(foundUser)); verify(filter1).searchForUser("username"); verify(filter2).searchForUser("username"); verify(logger).warn("The ldap configuration for search base 'base1' is invalid", runtimeException); } @Test public void shouldLogErrorsForAllInvalidSearchBaseWhenAuthenticating() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter3 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter4 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"), new BaseConfig("base3"), new BaseConfig("base4"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); doReturn(filter3).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(2).getValue(), ldapConfig.searchFilter()); doReturn(filter4).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(3).getValue(), ldapConfig.searchFilter()); DirContextOperations foundUser = mock(DirContextOperations.class); RuntimeException runtimeException = new RuntimeException("Invalid search base"); when(filter1.searchForUser("username")).thenThrow(runtimeException); when(filter2.searchForUser("username")).thenThrow(new UsernameNotFoundException("User not found")); when(filter3.searchForUser("username")).thenThrow(runtimeException); when(filter4.searchForUser("username")).thenReturn(foundUser); assertThat(spy.searchForUser("username"), is(foundUser)); verify(logger, times(1)).warn("The ldap configuration for search base 'base1' is invalid", runtimeException); verify(logger, times(1)).warn("The ldap configuration for search base 'base3' is invalid", runtimeException); } @Test public void shouldNotLogWhenLastSearchBaseIsInvalidAndUserIsNotFound() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); RuntimeException runtimeException = new RuntimeException("Invalid search base"); when(filter1.searchForUser("username")).thenThrow(new UsernameNotFoundException("User not found")); when(filter2.searchForUser("username")).thenThrow(runtimeException); thrown.expect(RuntimeException.class); spy.searchForUser("username"); verify(logger, never()).warn(Matchers.<Object>any(), Matchers.<Throwable>any()); } @Test public void shouldNotLogErrorsIfThereIsOnlyOneSearchBaseWhichIsInvalidWhenAuthenticating() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"))); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); RuntimeException runtimeException = new RuntimeException("Invalid search base"); when(filter1.searchForUser("username")).thenThrow(runtimeException); thrown.expect(RuntimeException.class); spy.searchForUser("username"); verify(logger, never()).warn(Matchers.<Object>any(), Matchers.<Throwable>any()); } @Test public void shouldNotLogWhenUserNameNotFoundExceptionIsThrown() { final FilterBasedLdapUserSearch filter1 = mock(FilterBasedLdapUserSearch.class); final FilterBasedLdapUserSearch filter2 = mock(FilterBasedLdapUserSearch.class); LdapConfig ldapConfig = setLdapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2"))); DirContextOperations foundUser = mock(DirContextOperations.class); doReturn(filter1).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(0).getValue(), ldapConfig.searchFilter()); doReturn(filter2).when(spy).getFilterBasedLdapUserSearch(ldapConfig.getBasesConfig().get(1).getValue(), ldapConfig.searchFilter()); when(filter1.searchForUser("username")).thenThrow(new UsernameNotFoundException("User username not found in directory.")); when(filter2.searchForUser("username")).thenReturn(foundUser); spy.searchForUser("username"); verify(logger, never()).warn(Matchers.<Object>any(), Matchers.<Throwable>any()); } @Test public void shouldThrowUpWhenNoSearchBaseIsConfigured() { setLdapConfig(new BasesConfig()); thrown.expect(RuntimeException.class); thrown.expectMessage(is("No LDAP Search Bases are configured.")); spy.searchForUser("username"); } @Test public void shouldFilterForMatchingUsernamesInSearchBase() throws Exception { ldapUserSearch.search("username", ldapConfig(new BasesConfig(new BaseConfig("base1")))); verify(ldapTemplate).search(argThat(is("base1")),anyString(),any(SearchControls.class),any(AttributesMapperCallbackHandler.class)); } @Test public void shouldFilterForMatchingUsernamesInMultipleBases() throws Exception { AttributesMapperCallbackHandler handler = mock(AttributesMapperCallbackHandler.class); doReturn(handler).when(spy).getAttributesMapperCallbackHandler(); when(handler.getList()).thenReturn(Arrays.asList()); spy.search("username", ldapConfig(new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2")))); verify(handler).getList(); verify(ldapTemplate).search(argThat(is("base1")), anyString(), any(SearchControls.class), eq(handler)); verify(ldapTemplate).search(argThat(is("base2")), anyString(), any(SearchControls.class), eq(handler)); } @Test public void shouldThrowExceptionWhenSearchingIfBaseSearchIsEmpty(){ setLdapConfig(new BasesConfig()); thrown.expect(RuntimeException.class); thrown.expectMessage(is("Atleast one Search Base needs to be configured.")); spy.search("username"); } private LdapConfig setLdapConfig(final BasesConfig basesConfig) { when(securityConfig.isSecurityEnabled()).thenReturn(true); LdapConfig ldapConfig = ldapConfig(basesConfig); when(securityConfig.ldapConfig()).thenReturn(ldapConfig); return ldapConfig; } private LdapConfig ldapConfig(BasesConfig basesConfig) { return new LdapConfig("url", "managerDN", "managerPassword", "encrypted", false, basesConfig, "searchFilter"); } }