/*
* Copyright 2016 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.service;
import com.thoughtworks.go.config.*;
import com.thoughtworks.go.config.server.security.ldap.BaseConfig;
import com.thoughtworks.go.config.server.security.ldap.BasesConfig;
import com.thoughtworks.go.domain.ServerSiteUrlConfig;
import com.thoughtworks.go.i18n.Localizer;
import com.thoughtworks.go.security.GoCipher;
import com.thoughtworks.go.server.security.InMemoryLdapServerForTests;
import com.thoughtworks.go.server.service.result.HttpLocalizedOperationResult;
import com.thoughtworks.go.util.GoConfigFileHelper;
import com.unboundid.ldif.LDIFRecord;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.io.IOException;
import java.net.URISyntaxException;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.assertThat;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
"classpath:WEB-INF/applicationContext-global.xml",
"classpath:WEB-INF/applicationContext-dataLocalAccess.xml",
"classpath:WEB-INF/applicationContext-acegi-security.xml"
})
public class ServerConfigServiceIntegrationTest {
@Autowired
GoConfigService goConfigService;
@Autowired ServerConfigService serverConfigService;
@Autowired UserService userService;
@Autowired
GoConfigDao goConfigDao;
@Autowired Localizer localizer;
private GoConfigFileHelper configHelper = new GoConfigFileHelper();
private InMemoryLdapServerForTests ldapServer;
private LDIFRecord employeesOrgUnit;
private static final int PORT = 12389;
private static final String LDAP_URL = "ldap://localhost:" + PORT;
private static final String BASE_DN = "dc=corp,dc=somecompany,dc=com";
private static final String MANAGER_DN = "cn=Active Directory Ldap User,ou=SomeSystems,ou=Accounts,ou=Principal," + BASE_DN;
private static final String MANAGER_PASSWORD = "some-password";
private static final String SEARCH_BASE = "ou=Employees,ou=Company,ou=Principal," + BASE_DN;
private static final String SEARCH_FILTER = "(sAMAccountName={0})";
@Before
public void setup() throws Exception {
configHelper.usingCruiseConfigDao(goConfigDao).initializeConfigFile();
configHelper.onSetUp();
goConfigService.forceNotifyListeners();
ldapServer = new InMemoryLdapServerForTests(BASE_DN, MANAGER_DN, MANAGER_PASSWORD).start(PORT);
ldapServer.addOrganizationalUnit("Principal", "ou=Principal," + BASE_DN);
ldapServer.addOrganizationalUnit("Company", "ou=Company,ou=Principal," + BASE_DN);
employeesOrgUnit = ldapServer.addOrganizationalUnit("Employees", "ou=Employees,ou=Company,ou=Principal," + BASE_DN);
}
@After
public void tearDown() throws Exception {
ldapServer.stop();
configHelper.onTearDown();
}
@Test
public void shouldUpdateServerConfigWithoutValidatingMailHostWhenMailhostisEmpty() {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
PasswordFileConfig passwordFileConfig = new PasswordFileConfig("valid_path.txt");
MailHost mailHost = new MailHost(new GoCipher());
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordFileConfig, "artifacts", null, null, "42", true, "http://site_url", "https://secure_site_url", "default", result,
goConfigDao.md5OfConfigFile());
assertThat(goConfigService.getMailHost(), is(mailHost));
assertThat(goConfigService.security(), is(new SecurityConfig(ldapConfig, passwordFileConfig, false)));
assertThat(result.isSuccessful(), is(true));
}
@Test
public void shouldUpdateServerConfig() throws InvalidCipherTextException {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
PasswordFileConfig passwordFileConfig = new PasswordFileConfig("valid_path.txt");
MailHost mailHost = new MailHost("boo", 1, "username", "password", new GoCipher().encrypt("password"), true, true, "from@from.com", "admin@admin.com", new GoCipher());
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordFileConfig, "newArtifactsDir", 10.0, 20.0, "42", true, "http://site_url", "https://secure_site_url", "gist-repo/folder",
result, goConfigDao.md5OfConfigFile());
assertThat(goConfigService.getMailHost(), is(mailHost));
assertThat(goConfigService.security(), is(new SecurityConfig(ldapConfig, passwordFileConfig, false)));
assertThat(goConfigService.serverConfig().artifactsDir(), is("newArtifactsDir"));
assertThat(goConfigService.serverConfig().getSiteUrl().getUrl(), is("http://site_url"));
assertThat(goConfigService.serverConfig().getSecureSiteUrl().getUrl(), is("https://secure_site_url"));
assertThat(goConfigService.serverConfig().getJobTimeout(), is("42"));
assertThat(goConfigService.serverConfig().getPurgeStart(), is(10.0));
assertThat(goConfigService.serverConfig().getPurgeUpto(), is(20.0));
assertThat(goConfigService.serverConfig().getCommandRepositoryLocation(),is("gist-repo/folder"));
assertThat(result.isSuccessful(), is(true));
}
@Test
public void shouldAllowNullValuesForPurgeStartAndPurgeUpTo() throws InvalidCipherTextException {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
PasswordFileConfig passwordFileConfig = new PasswordFileConfig("valid_path.txt");
MailHost mailHost = new MailHost("boo", 1, "username", "password", new GoCipher().encrypt("password"), true, true, "from@from.com", "admin@admin.com", new GoCipher());
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordFileConfig, "newArtifactsDir", null, null, "42", true, "http://site_url", "https://secure_site_url", "default",
result,
goConfigDao.md5OfConfigFile());
assertThat(goConfigService.serverConfig().getPurgeStart(), is(nullValue()));
assertThat(goConfigService.serverConfig().getPurgeUpto(), is(nullValue()));
}
@Test
public void shouldUpdateWithEmptySecureSiteUrlAndSiteUrl() throws InvalidCipherTextException {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
PasswordFileConfig passwordFileConfig = new PasswordFileConfig("valid_path.txt");
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
MailHost mailHost = new MailHost("boo", 1, "username", "password", new GoCipher().encrypt("password"), true, true, "from@from.com", "admin@admin.com", new GoCipher());
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordFileConfig, "newArtifactsDir", null, null, "42", true, "", "", "default", result,
goConfigDao.md5OfConfigFile());
assertThat(goConfigService.serverConfig().getSiteUrl(), is(new ServerSiteUrlConfig()));
assertThat(goConfigService.serverConfig().getSecureSiteUrl(), is(new ServerSiteUrlConfig()));
}
@Test
public void update_shouldKeepTheExistingSecurityAsIs() throws IOException {
Role role = new RoleConfig(new CaseInsensitiveString("awesome"), new RoleUser(new CaseInsensitiveString("first")));
configHelper.turnOnSecurity();
configHelper.addRole(role);
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
PasswordFileConfig passwordConfig = new PasswordFileConfig("valid_path.txt");
SecurityConfig securityConfig = createSecurity(role, ldapConfig, passwordConfig, false);
MailHost mailHost = new MailHost("boo", 1, "username", "password", true, true, "from@from.com", "admin@admin.com");
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordConfig, "artifacts", null, null, "42", true, "http://site_url", "https://secure_site_url", "default", result,
goConfigDao.md5OfConfigFile());
assertThat(goConfigService.getMailHost(), is(mailHost));
assertThat(goConfigService.security(), is(securityConfig));
assertThat(result.isSuccessful(), is(true));
}
private SecurityConfig createSecurity(Role role, LdapConfig ldapConfig, PasswordFileConfig passwordConfig, boolean allowOnlyKnownUsersToLogin) {
SecurityConfig securityConfig = new SecurityConfig();
securityConfig.addRole(role);
securityConfig.modifyLdap(ldapConfig);
securityConfig.modifyPasswordFile(passwordConfig);
securityConfig.modifyAllowOnlyKnownUsers(allowOnlyKnownUsersToLogin);
return securityConfig;
}
@Test
public void shouldUpdateServerConfigShouldFailWhenConfigSaveFails() {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig("")), "");
PasswordFileConfig passwordFileConfig = new PasswordFileConfig("valid_path.txt");
MailHost mailHost = new MailHost("boo", 1, "username", "password", true, true, "from@from.com", "admin@admin.com");
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(mailHost, ldapConfig, passwordFileConfig, "artifacts", null, null, "42", true, "http://site_url", "https://secure_site_url", "default", result,
goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), containsString("Failed to save the server configuration. Reason: "));
assertThat(result.message(localizer), containsString("Search Base should not be empty"));
}
@Test
public void shouldUpdateOnlyLdapConfiguration() {
CruiseConfig cruiseConfig = goConfigDao.loadForEditing();
LdapConfig newLdapConfig = new LdapConfig("url", "managerDN", "managerPassword", "encrypted", true, new BasesConfig(new BaseConfig("base1"), new BaseConfig("base2")), "filter");
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
ServerConfig serverConfig = cruiseConfig.server();
serverConfigService.updateServerConfig(cruiseConfig.mailHost(), newLdapConfig, serverConfig.security().passwordFileConfig(),
serverConfig.artifactsDir(), serverConfig.getPurgeStart(), serverConfig.getPurgeUpto(), serverConfig.getJobTimeout(), true,
serverConfig.getSiteUrl().getUrl(), serverConfig.getSecureSiteUrl().getUrl(), serverConfig.getCommandRepositoryLocation(), result, cruiseConfig.getMd5());
goConfigDao.forceReload();
CruiseConfig updatedCruiseConfig = goConfigDao.loadForEditing();
assertThat(result.isSuccessful(), is(true));
assertThat(updatedCruiseConfig.server().security().ldapConfig().isEnabled(), is(true));
}
@Test
public void updateServerConfig_ShouldFailWhenAllowAutoLoginIsTurnedOffWithNoAdminsRemaining() throws IOException {
configHelper.turnOnSecurity();
userService.deleteAll();
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(new MailHost(new GoCipher()), new LdapConfig(new GoCipher()), new PasswordFileConfig(), "artifacts", null, null, "42",
false,
"http://site_url", "https://secure_site_url", "default", result, goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), containsString("Cannot disable auto login with no admins enabled."));
}
@Test
public void shouldNotUpdateWhenHostnameIsInvalid() {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(new MailHost("boo%bee", 1, "username", "password", true, true, "from@from.com", "admin@admin.com"),
new LdapConfig(new GoCipher()),
new PasswordFileConfig(), "artifacts", null, null, "42", true, "http://site_url", "https://secure_site_url", "default", result, goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Invalid hostname. A valid hostname can only contain letters (A-z) digits (0-9) hyphens (-) dots (.) and underscores (_)."));
assertThat(goConfigService.getMailHost(), is(new MailHost(new GoCipher())));
}
@Test
public void shouldNotUpdateWhenPortIsInvalid() {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(new MailHost("boo", -1, "username", "password", true, true, "from@from.com", "admin@admin.com"), new LdapConfig(new GoCipher()),
new PasswordFileConfig(), "artifacts", null, null, "42", true, "http://site_url", "https://secure_site_url", "default", result, goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Invalid port."));
assertThat(goConfigService.getMailHost(), is(new MailHost(new GoCipher())));
}
@Test
public void shouldNotUpdateWhenEmailIsInvalid() {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(new MailHost("boo", 1, "username", "password", true, true, "from", "admin@admin.com"), new LdapConfig(new GoCipher()),
new PasswordFileConfig(),
"artifacts", null, null, "42", true,
"http://site_url", "https://secure_site_url", "default", result, goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("From address is not a valid email address."));
assertThat(goConfigService.getMailHost(), is(new MailHost(new GoCipher())));
}
@Test
public void shouldNotUpdateWhenAdminEmailIsInvalid() {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
serverConfigService.updateServerConfig(new MailHost("boo", 1, "username", "password", true, true, "from@from.com", "admin"), new LdapConfig(new GoCipher()),
new PasswordFileConfig(),
"artifacts", null, null, "42", true,
"http://site_url", "https://secure_site_url", "default", result, goConfigDao.md5OfConfigFile());
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Admin address is not a valid email address."));
assertThat(goConfigService.getMailHost(), is(new MailHost(new GoCipher())));
}
@Test
public void shouldReturnErrorResultWhenLdapSearchFails() throws Exception {
HttpLocalizedOperationResult result = new HttpLocalizedOperationResult();
LdapConfig invalidLdapConfig = new LdapConfig(new GoCipher());
serverConfigService.validateLdapSettings(invalidLdapConfig, result);
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Cannot connect to ldap, please check the settings. Reason: An LDAP connection URL must be supplied."));
result = new HttpLocalizedOperationResult();
invalidLdapConfig = new LdapConfig("ldap://some_loser_url", MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
serverConfigService.validateLdapSettings(invalidLdapConfig, result);
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer),
is("Cannot connect to ldap, please check the settings. Reason: some_loser_url:389; nested exception is javax.naming.CommunicationException: some_loser_url:389 [Root exception is java.net.UnknownHostException: some_loser_url]"));
result = new HttpLocalizedOperationResult();
invalidLdapConfig = new LdapConfig(LDAP_URL, "invalidDN=1", MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
serverConfigService.validateLdapSettings(invalidLdapConfig, result);
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Cannot connect to ldap, please check the settings." +
" Reason: [LDAP: error code 49 - Unable to bind as user 'invalidDN=1' because no such entry" +
" exists in the server.]; nested exception is javax.naming.AuthenticationException:" +
" [LDAP: error code 49 - Unable to bind as user 'invalidDN=1' because no such entry exists in the server.]"));
result = new HttpLocalizedOperationResult();
invalidLdapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, "wrong_password", null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
serverConfigService.validateLdapSettings(invalidLdapConfig, result);
assertThat(result.isSuccessful(), is(false));
assertThat(result.message(localizer), is("Cannot connect to ldap, please check the settings." +
" Reason: [LDAP: error code 49 - Unable to bind as user 'cn=Active Directory Ldap User," +
"ou=SomeSystems,ou=Accounts,ou=Principal,dc=corp,dc=somecompany,dc=com' because the provided" +
" password was incorrect.]; nested exception is javax.naming.AuthenticationException:" +
" [LDAP: error code 49 - Unable to bind as user 'cn=Active Directory Ldap User," +
"ou=SomeSystems,ou=Accounts,ou=Principal,dc=corp,dc=somecompany,dc=com' because the provided" +
" password was incorrect.]"));
result = new HttpLocalizedOperationResult();
LdapConfig validConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, null, true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
serverConfigService.validateLdapSettings(validConfig, result);
assertThat("Expected no message. Got: " + result.message(localizer), result.isSuccessful(), is(true));
}
@Test
public void shouldSiteUrlForGivenUrl() throws URISyntaxException {
configHelper.setBaseUrls(new ServerSiteUrlConfig("http://foo.com"), new ServerSiteUrlConfig("https://bar.com"));
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", true), is("https://bar.com/foo/bar"));
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", false), is("http://foo.com/foo/bar"));
}
@Test
public void shouldReturnTheSameURLWhenNothingIsConfigured() throws URISyntaxException {
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", true), is("http://test.host/foo/bar"));
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", false), is("http://test.host/foo/bar"));
}
@Test
public void shouldUseTheSiteUrlWhenSecureSiteUrlIsNotPresentAndOnlyIfSiteUrlIsHttps() throws URISyntaxException {
configHelper.setBaseUrls(new ServerSiteUrlConfig("https://foo.com"), new ServerSiteUrlConfig());
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", true), is("https://foo.com/foo/bar"));
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", false), is("https://foo.com/foo/bar"));
}
@Test
public void shouldUseTheSecureSiteUrlInspiteOfCallerNotForcingSsl_whenAlreadyUsingHTTPS() throws URISyntaxException {
configHelper.setBaseUrls(new ServerSiteUrlConfig("http://foo.com:80"), new ServerSiteUrlConfig("https://bar.com:443"));
assertThat(serverConfigService.siteUrlFor("https://test.host:1000/foo/bar", false), is("https://bar.com:443/foo/bar"));
assertThat(serverConfigService.siteUrlFor("http://test.host/foo/bar", false), is("http://foo.com:80/foo/bar"));
}
@Test
public void shouldUseTheNewPasswordIfItIsChanged() {
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, "changed_password", "encrypted_password", true, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
DefaultSpringSecurityContextSource source = serverConfigService.ldapContextSource(ldapConfig);
assertThat(source.getAuthenticationSource().getCredentials(), is("changed_password"));
}
@Test
public void shouldUseTheEncryptedPasswordWhenPasswordIsNotChanged() throws InvalidCipherTextException {
String encryptedPassword = new GoCipher().encrypt("encrypted_password");
LdapConfig ldapConfig = new LdapConfig(LDAP_URL, MANAGER_DN, MANAGER_PASSWORD, encryptedPassword, false, new BasesConfig(new BaseConfig(SEARCH_BASE)), SEARCH_FILTER);
DefaultSpringSecurityContextSource source = serverConfigService.ldapContextSource(ldapConfig);
assertThat(source.getAuthenticationSource().getCredentials(), is("encrypted_password"));
}
}