Integrate Plugin Center myCloudogu Authentication (#1884)

Allows scm-manager instances to authenticate with the configured plugin center. If the default plugin center is used, a myCloudogu account is used for authentication which in turn enables downloading special myCloudogu plugins directly through the plugin administration page.

Co-authored-by: Konstantin Schaper <konstantin.schaper@cloudogu.com>
Co-authored-by: Matthias Thieroff <93515444+mthieroff@users.noreply.github.com>
Co-authored-by: Philipp Ahrendt <philipp.ahrendt@cloudogu.com>
This commit is contained in:
Sebastian Sdorra
2021-12-13 15:15:57 +01:00
committed by GitHub
parent c95888d491
commit 6eba01161f
84 changed files with 3147 additions and 289 deletions

View File

@@ -93,7 +93,7 @@ class AvailablePluginResourceTest {
@BeforeEach
void prepareEnvironment() {
pluginRootResource = new PluginRootResource(null, availablePluginResourceProvider, null);
pluginRootResource = new PluginRootResource(null, availablePluginResourceProvider, null, null);
when(availablePluginResourceProvider.get()).thenReturn(availablePluginResource);
dispatcher.addSingletonResource(pluginRootResource);
}

View File

@@ -24,21 +24,18 @@
package sonia.scm.api.v2.resources;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.internal.util.collections.Sets;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.MockitoAnnotations.initMocks;
public class ConfigDtoToScmConfigurationMapperTest {
@ExtendWith(MockitoExtension.class)
class ConfigDtoToScmConfigurationMapperTest {
@InjectMocks
private ConfigDtoToScmConfigurationMapperImpl mapper;
@@ -46,53 +43,49 @@ public class ConfigDtoToScmConfigurationMapperTest {
private final String[] expectedExcludes = {"ex", "clude"};
private final String[] expectedUsers = {"trillian", "arthur"};
@Before
public void init() {
initMocks(this);
}
@Test
public void shouldMapFields() {
void shouldMapFields() {
ConfigDto dto = createDefaultDto();
ScmConfiguration config = mapper.map(dto);
assertEquals("prPw", config.getProxyPassword());
assertEquals(42, config.getProxyPort());
assertEquals("srvr", config.getProxyServer());
assertEquals("user", config.getProxyUser());
assertTrue(config.isEnableProxy());
assertEquals("realm", config.getRealmDescription());
assertTrue(config.isDisableGroupingGrid());
assertEquals("yyyy", config.getDateFormat());
assertEquals(AnonymousMode.PROTOCOL_ONLY, config.getAnonymousMode());
assertEquals("baseurl", config.getBaseUrl());
assertTrue(config.isForceBaseUrl());
assertEquals(41, config.getLoginAttemptLimit());
assertTrue("proxyExcludes", config.getProxyExcludes().containsAll(Arrays.asList(expectedExcludes)));
assertTrue(config.isSkipFailedAuthenticators());
assertEquals("https://plug.ins", config.getPluginUrl());
assertEquals(40, config.getLoginAttemptLimitTimeout());
assertTrue(config.isEnabledXsrfProtection());
assertFalse(config.isEnabledUserConverter());
assertEquals("username", config.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
assertEquals("hitchhiker.mail", config.getMailDomainName());
assertTrue("emergencyContacts", config.getEmergencyContacts().containsAll(Arrays.asList(expectedUsers)));
assertThat(config.getProxyPassword()).isEqualTo("prPw");
assertThat(config.getProxyPort()).isEqualTo(42);
assertThat(config.getProxyServer()).isEqualTo("srvr");
assertThat(config.getProxyUser()).isEqualTo("user");
assertThat(config.isEnableProxy()).isTrue();
assertThat(config.getRealmDescription()).isEqualTo("realm");
assertThat(config.isDisableGroupingGrid()).isTrue();
assertThat(config.getDateFormat()).isEqualTo("yyyy");
assertThat(config.getAnonymousMode()).isSameAs(AnonymousMode.PROTOCOL_ONLY);
assertThat(config.getBaseUrl()).isEqualTo("baseurl");
assertThat(config.isForceBaseUrl()).isTrue();
assertThat(config.getLoginAttemptLimit()).isEqualTo(41);
assertThat(config.getProxyExcludes()).contains(expectedExcludes);
assertThat(config.isSkipFailedAuthenticators()).isTrue();
assertThat(config.getPluginUrl()).isEqualTo("https://plug.ins");
assertThat(config.getPluginAuthUrl()).isEqualTo("https://plug.ins/oidc");
assertThat(config.getLoginAttemptLimitTimeout()).isEqualTo(40);
assertThat(config.isEnabledXsrfProtection()).isTrue();
assertThat(config.isEnabledUserConverter()).isFalse();
assertThat(config.getNamespaceStrategy()).isEqualTo("username");
assertThat(config.getLoginInfoUrl()).isEqualTo("https://scm-manager.org/login-info");
assertThat(config.getMailDomainName()).isEqualTo("hitchhiker.mail");
assertThat(config.getEmergencyContacts()).contains(expectedUsers);
}
@Test
public void shouldMapAnonymousAccessFieldToAnonymousMode() {
void shouldMapAnonymousAccessFieldToAnonymousMode() {
ConfigDto dto = createDefaultDto();
ScmConfiguration config = mapper.map(dto);
assertEquals(AnonymousMode.PROTOCOL_ONLY, config.getAnonymousMode());
assertThat(config.getAnonymousMode()).isSameAs(AnonymousMode.PROTOCOL_ONLY);
dto.setAnonymousMode(null);
dto.setAnonymousAccessEnabled(false);
ScmConfiguration config2 = mapper.map(dto);
assertEquals(AnonymousMode.OFF, config2.getAnonymousMode());
assertThat(config2.getAnonymousMode()).isSameAs(AnonymousMode.OFF);
}
private ConfigDto createDefaultDto() {
@@ -112,6 +105,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setProxyExcludes(Sets.newSet(expectedExcludes));
configDto.setSkipFailedAuthenticators(true);
configDto.setPluginUrl("https://plug.ins");
configDto.setPluginAuthUrl("https://plug.ins/oidc");
configDto.setLoginAttemptLimitTimeout(40);
configDto.setEnabledXsrfProtection(true);
configDto.setNamespaceStrategy("username");

View File

@@ -33,6 +33,7 @@ import org.junit.Test;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.initialization.InitializationFinisher;
import sonia.scm.plugin.PluginCenterAuthenticator;
import sonia.scm.search.SearchEngine;
import java.net.URI;
@@ -51,7 +52,6 @@ public class IndexResourceTest {
private SCMContextProvider scmContextProvider;
private IndexResource indexResource;
@Before
public void setUpObjectUnderTest() {
this.configuration = new ScmConfiguration();
@@ -63,10 +63,28 @@ public class IndexResourceTest {
ResourceLinksMock.createMock(URI.create("/")),
scmContextProvider,
configuration,
initializationFinisher, searchEngine);
initializationFinisher,
searchEngine
);
this.indexResource = new IndexResource(generator);
}
@Test
@SubjectAware(username = "dent", password = "secret")
public void shouldRenderPluginCenterAuthLink() {
IndexDto index = indexResource.getIndex();
Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isPresent();
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void shouldNotRenderPluginCenterLoginLinkIfPermissionsAreMissing() {
IndexDto index = indexResource.getIndex();
Assertions.assertThat(index.getLinks().getLinkBy("pluginCenterAuth")).isNotPresent();
}
@Test
public void shouldRenderLoginUrlsForUnauthenticatedRequest() {
IndexDto index = indexResource.getIndex();

View File

@@ -89,7 +89,7 @@ class InstalledPluginResourceTest {
@BeforeEach
void prepareEnvironment() {
pluginRootResource = new PluginRootResource(installedPluginResourceProvider, null, null);
pluginRootResource = new PluginRootResource(installedPluginResourceProvider, null, null, null);
when(installedPluginResourceProvider.get()).thenReturn(installedPluginResource);
dispatcher.addSingletonResource(pluginRootResource);
}

View File

@@ -90,7 +90,7 @@ class PendingPluginResourceTest {
@BeforeEach
void prepareEnvironment() {
dispatcher.registerException(ShiroException.class, Response.Status.UNAUTHORIZED);
PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource));
PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource), null);
dispatcher.addSingletonResource(pluginRootResource);
}

View File

@@ -0,0 +1,469 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.inject.util.Providers;
import lombok.Value;
import org.github.sdorra.jse.ShiroExtension;
import org.github.sdorra.jse.SubjectAware;
import org.jboss.resteasy.mock.MockHttpRequest;
import org.jboss.resteasy.mock.MockHttpResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.AuthenticationInfo;
import sonia.scm.plugin.FetchAccessTokenFailedException;
import sonia.scm.plugin.PluginCenterAuthenticator;
import sonia.scm.security.Impersonator;
import sonia.scm.security.SecureParameterSerializer;
import sonia.scm.security.XsrfExcludes;
import sonia.scm.user.DisplayUser;
import sonia.scm.user.UserDisplayManager;
import sonia.scm.user.UserTestData;
import sonia.scm.web.RestDispatcher;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.time.Instant;
import java.util.Optional;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
import static sonia.scm.api.v2.resources.PluginCenterAuthResource.*;
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
class PluginCenterAuthResourceTest {
private final RestDispatcher dispatcher = new RestDispatcher();
private final ScmConfiguration scmConfiguration = new ScmConfiguration();
@Mock
private PluginCenterAuthenticator authenticator;
@Mock
private XsrfExcludes excludes;
@Mock
private ChallengeGenerator challengeGenerator;
@Mock
private UserDisplayManager userDisplayManager;
@Mock
private SecureParameterSerializer parameterSerializer;
@Mock
private Impersonator impersonator;
@BeforeEach
void setUpDispatcher() {
ScmPathInfoStore pathInfoStore = new ScmPathInfoStore();
pathInfoStore.set(rootPathInfo);
PluginCenterAuthResource resource = new PluginCenterAuthResource(
pathInfoStore, authenticator, userDisplayManager,
scmConfiguration, excludes, challengeGenerator,
parameterSerializer, impersonator
);
dispatcher.addSingletonResource(
new PluginRootResource(
null,
null,
null,
Providers.of(resource)
)
);
}
@Nested
class GetAuthenticationInfo {
@Test
void shouldReturnEmptyAuthenticationInfo() throws URISyntaxException, IOException {
JsonNode root = getJson("/v2/plugins/auth");
assertThat(root.has("principal")).isFalse();
assertThat(root.has("pluginCenterSubject")).isFalse();
assertThat(root.has("date")).isFalse();
assertThat(root.get("_links").get("self").get("href").asText()).isEqualTo("/v2/plugins/auth");
}
@Test
void shouldReturnTrueForIsDefault() throws URISyntaxException, IOException {
JsonNode root = getJson("/v2/plugins/auth");
assertThat(root.get("default").asBoolean()).isTrue();
}
@Test
void shouldReturnFalseIfTheAuthUrlIsNotDefault() throws URISyntaxException, IOException {
scmConfiguration.setPluginAuthUrl("https://plug.ins");
JsonNode root = getJson("/v2/plugins/auth");
assertThat(root.get("default").asBoolean()).isFalse();
}
@Test
@SubjectAware(value = "marvin", permissions = "plugin:write")
void shouldReturnLoginLinkIfPermitted() throws URISyntaxException, IOException {
JsonNode root = getJson("/v2/plugins/auth");
assertThat(root.get("_links").get("login").get("href").asText()).isEqualTo("/v2/plugins/auth/login");
}
@Test
void shouldReturnAuthenticationInfo() throws IOException, URISyntaxException {
JsonNode root = requestAuthInfo();
assertThat(root.get("principal").asText()).isEqualTo("Tricia McMillan");
assertThat(root.get("pluginCenterSubject").asText()).isEqualTo("tricia.mcmillan@hitchhiker.com");
assertThat(root.get("date").asText()).isNotEmpty();
assertThat(root.get("_links").get("self").get("href").asText()).isEqualTo("/v2/plugins/auth");
}
@Test
void shouldNotReturnLogoutLinkWithoutWritePermission() throws IOException, URISyntaxException {
JsonNode root = requestAuthInfo();
assertThat(root.get("_links").has("logout")).isFalse();
}
@Test
@SubjectAware(value = "marvin", permissions = "plugin:write")
void shouldReturnLogoutLinkIfPermitted() throws IOException, URISyntaxException {
JsonNode root = requestAuthInfo();
assertThat(root.get("_links").get("logout").get("href").asText()).isEqualTo("/v2/plugins/auth");
}
@Test
@SubjectAware(value = "marvin", permissions = "plugin:write")
void shouldNotReturnLogoutLinkIfPermitted() throws IOException, URISyntaxException {
JsonNode root = requestAuthInfo();
assertThat(root.get("_links").get("logout").get("href").asText()).isEqualTo("/v2/plugins/auth");
}
private JsonNode requestAuthInfo() throws IOException, URISyntaxException {
AuthenticationInfo info = new SimpleAuthenticationInfo(
"trillian", "tricia.mcmillan@hitchhiker.com", Instant.now()
);
when(authenticator.getAuthenticationInfo()).thenReturn(Optional.of(info));
DisplayUser user = DisplayUser.from(UserTestData.createTrillian());
when(userDisplayManager.get("trillian")).thenReturn(Optional.of(user));
return getJson("/v2/plugins/auth");
}
}
@Nested
class Logout {
@Test
void shouldLogout() throws URISyntaxException {
MockHttpResponse response = request(MockHttpRequest.delete("/v2/plugins/auth"));
verify(authenticator).logout();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NO_CONTENT);
}
}
@Nested
class AuthRequest {
@Test
void shouldReturnErrorRedirectWithoutSourceParameter() throws URISyntaxException {
MockHttpResponse response = get("/v2/plugins/auth/login");
assertError(response, ERROR_SOURCE_MISSING);
}
@Test
void shouldReturnErrorRedirectWithoutPluginAuthUrlParameter() throws URISyntaxException {
scmConfiguration.setPluginAuthUrl("");
MockHttpResponse response = get("/v2/plugins/auth/login?source=/admin/plugins");
assertError(response, ERROR_AUTHENTICATION_DISABLED);
}
@Test
void shouldReturnErrorRedirectIfAlreadyAuthenticated() throws URISyntaxException {
when(authenticator.isAuthenticated()).thenReturn(true);
MockHttpResponse response = get("/v2/plugins/auth/login?source=/admin/plugins");
assertError(response, ERROR_ALREADY_AUTHENTICATED);
}
@Test
@SubjectAware("trillian")
void shouldReturnRedirectToPluginAuthUrl() throws URISyntaxException, IOException {
when(challengeGenerator.create()).thenReturn("abcd");
when(parameterSerializer.serialize(any(AuthParameter.class))).thenReturn("def");
scmConfiguration.setPluginAuthUrl("https://plug.ins");
MockHttpResponse response = get("/v2/plugins/auth/login?source=/admin/plugins");
assertRedirect(response, "https://plug.ins?instance=%2Fv2%2Fplugins%2Fauth%2Fcallback?params%3Ddef");
}
@Test
@SubjectAware("trillian")
void shouldExcludeCallbackFromXsrf() throws URISyntaxException, IOException {
when(challengeGenerator.create()).thenReturn("1234");
when(parameterSerializer.serialize(any(AuthParameter.class))).thenReturn("def");
scmConfiguration.setPluginAuthUrl("https://plug.ins");
get("/v2/plugins/auth/login?source=/admin/plugins");
verify(excludes).add("/v2/plugins/auth/callback");
}
@Test
@SubjectAware("trillian")
void shouldSendAuthParameters() throws URISyntaxException, IOException {
when(challengeGenerator.create()).thenReturn("abc123def");
when(parameterSerializer.serialize(any(AuthParameter.class))).thenReturn("xyz");
get("/v2/plugins/auth/login?source=/admin/plugins");
ArgumentCaptor<AuthParameter> captor = ArgumentCaptor.forClass(AuthParameter.class);
verify(parameterSerializer).serialize(captor.capture());
AuthParameter parameter = captor.getValue();
assertThat(parameter.getChallenge()).isEqualTo("abc123def");
assertThat(parameter.getSource()).isEqualTo("/admin/plugins");
assertThat(parameter.getPrincipal()).isEqualTo("trillian");
}
}
@Nested
@SubjectAware("marvin")
class AbortAuthentication {
@BeforeEach
void setUp() throws IOException {
lenient().when(challengeGenerator.create()).thenReturn("xyz");
lenient().when(parameterSerializer.serialize(any(AuthParameter.class))).thenReturn("secureParams");
}
@Test
void shouldReturnErrorRedirectWithoutParams() throws URISyntaxException {
MockHttpResponse response = get("/v2/plugins/auth/callback");
assertError(response, ERROR_PARAMS_MISSING);
}
@Test
void shouldReturnErrorRedirectWithoutChallenge() throws URISyntaxException, IOException {
mockParams("marvin", null, "/");
MockHttpResponse response = get("/v2/plugins/auth/callback?params=secureParams");
assertError(response, ERROR_CHALLENGE_MISSING);
}
@Test
void shouldReturnErrorRedirectWithChallengeMismatch() throws URISyntaxException, IOException {
mockParams("marvin", "abc", "/repos");
get("/v2/plugins/auth/login?source=/repos");
MockHttpResponse response = get("/v2/plugins/auth/callback?params=secureParams");
assertError(response, ERROR_CHALLENGE_DOES_NOT_MATCH);
}
@Test
void shouldRedirectToRoot() throws URISyntaxException, IOException {
mockParams("marvin", "xyz", null);
get("/v2/plugins/auth/login?source=/repos");
MockHttpResponse response = get("/v2/plugins/auth/callback?params=secureParams");
assertRedirect(response, "/");
}
@Test
void shouldRedirectToSource() throws URISyntaxException, IOException {
mockParams("marvin", "xyz", "/repos");
get("/v2/plugins/auth/login?source=/repos");
MockHttpResponse response = get("/v2/plugins/auth/callback?params=secureParams");
assertRedirect(response, "/repos");
}
@Test
void shouldRemoveCallbackFromXsrf() throws URISyntaxException, IOException {
mockParams("marvin", "xyz", "/repos");
get("/v2/plugins/auth/login?source=/repos");
get("/v2/plugins/auth/callback?params=secureParams");
verify(excludes).remove("/v2/plugins/auth/callback");
}
}
@Nested
@SubjectAware("slarti")
class AuthenticationCallback {
@BeforeEach
void setUp() throws IOException {
lenient().when(challengeGenerator.create()).thenReturn("abc");
lenient().when(parameterSerializer.serialize(any(AuthParameter.class))).thenReturn("secureParams");
}
@Test
void shouldReturnErrorRedirectWithoutParameters() throws URISyntaxException {
MockHttpResponse response = post("/v2/plugins/auth/callback", "trillian", "rf");
assertError(response, ERROR_PARAMS_MISSING);
}
@Test
void shouldReturnErrorRedirectWithoutChallengeParameter() throws URISyntaxException, IOException {
mockParams("slarti", null, "/");
MockHttpResponse response = post("/v2/plugins/auth/callback?params=secureParams", "slarti", "rf");
assertError(response, ERROR_CHALLENGE_MISSING);
}
@Test
void shouldReturnErrorRedirectWithChallengeMismatch() throws URISyntaxException, IOException {
mockParams("slarti", "xyz", "/");
get("/v2/plugins/auth/login?source=/repos");
MockHttpResponse response = post("/v2/plugins/auth/callback?params=secureParams", "trillian", "rf");
assertError(response, ERROR_CHALLENGE_DOES_NOT_MATCH);
}
@Test
void shouldReturnErrorRedirectFromFailedAuthentication() throws URISyntaxException, IOException {
mockParams("slarti", "abc", "/");
FetchAccessTokenFailedException exception = new FetchAccessTokenFailedException("failed ...");
doThrow(exception).when(authenticator).authenticate("slarti", "rf");
get("/v2/plugins/auth/login?source=/repos");
MockHttpResponse response = post("/v2/plugins/auth/callback?params=secureParams", "slarti", "rf");
assertError(response, exception.getCode());
}
@Test
void shouldAuthenticate() throws URISyntaxException, IOException {
mockParams("slarti", "abc", "/");
get("/v2/plugins/auth/login?source=/repos");
post("/v2/plugins/auth/callback?params=secureParams", "slarti", "refresh_token");
verify(authenticator).authenticate("slarti", "refresh_token");
}
@Test
void shouldRedirectToSource() throws URISyntaxException, IOException {
mockParams("slarti", "abc", "/users");
get("/v2/plugins/auth/login?source=/users");
MockHttpResponse response = post("/v2/plugins/auth/callback?params=secureParams", "slarti", "rrrrf");
assertRedirect(response, "/users");
}
@Test
void shouldRemoveCallbackFromXsrf() throws URISyntaxException, IOException {
mockParams("slarti", "abc", "/users");
get("/v2/plugins/auth/login?source=/repos");
post("/v2/plugins/auth/callback?params=secureParams", "slarti", "rf");
verify(excludes).remove("/v2/plugins/auth/callback");
}
}
private void mockParams(String principal, String challenge, String source) throws IOException {
AuthParameter params = new AuthParameter(principal, challenge, source);
when(parameterSerializer.deserialize("secureParams", AuthParameter.class)).thenReturn(params);
}
@CanIgnoreReturnValue
private MockHttpResponse post(String uri, String subject, String refreshToken) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.post(uri);
request.addFormHeader("subject", subject);
request.addFormHeader("refresh_token", refreshToken);
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
@CanIgnoreReturnValue
private MockHttpResponse get(String uri) throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.get(uri);
return request(request);
}
private final ObjectMapper mapper = new ObjectMapper();
private JsonNode getJson(String uri) throws URISyntaxException, IOException {
MockHttpResponse response = get(uri);
return mapper.readTree(response.getContentAsString());
}
private MockHttpResponse request(MockHttpRequest request) {
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
return response;
}
private void assertError(MockHttpResponse response, String code) {
assertRedirect(response, "/error/" + code);
}
private void assertRedirect(MockHttpResponse response, String location) {
assertRedirect(response, (locationHeader) -> assertThat(locationHeader).isEqualTo(location));
}
private void assertRedirect(MockHttpResponse response, Consumer<String> location) {
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_SEE_OTHER);
location.accept(response.getOutputHeaders().getFirst("Location").toString());
}
@Value
private static class SimpleAuthenticationInfo implements AuthenticationInfo {
String principal;
String pluginCenterSubject;
Instant date;
}
private static final ScmPathInfo rootPathInfo = new ScmPathInfo() {
@Override
public URI getApiRestUri() {
return URI.create("/api");
}
@Override
public URI getRootUri() {
return URI.create("/");
}
};
}

View File

@@ -165,6 +165,16 @@ class PluginDtoMapperTest {
.isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install?restart=true");
}
@Test
void shouldNotAppendInstallLinkWithEmptyDownloadUrl() {
when(subject.isPermitted("plugin:write")).thenReturn(true);
AvailablePlugin plugin = createAvailable(createPluginInformation(), "");
PluginDto dto = mapper.mapAvailable(plugin);
assertThat(dto.getLinks().hasLink("install")).isFalse();
assertThat(dto.getLinks().hasLink("installWithRestart")).isFalse();
}
@Test
void shouldReturnMiscellaneousIfCategoryIsNull() {
PluginInformation information = createPluginInformation();

View File

@@ -28,26 +28,28 @@ import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.support.SubjectThreadState;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.util.ThreadState;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.internal.util.collections.Sets;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.net.URI;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.initMocks;
public class ScmConfigurationToConfigDtoMapperTest {
@ExtendWith(MockitoExtension.class)
class ScmConfigurationToConfigDtoMapperTest {
private final URI baseUri = URI.create("http://example.com/base/");
@@ -65,78 +67,84 @@ public class ScmConfigurationToConfigDtoMapperTest {
private URI expectedBaseUri;
@Before
public void init() {
initMocks(this);
@BeforeEach
void init() {
expectedBaseUri = baseUri.resolve(ConfigResource.CONFIG_PATH_V2);
subjectThreadState.bind();
ThreadContext.bind(subject);
}
@After
@AfterEach
public void unbindSubject() {
ThreadContext.unbindSubject();
}
@Test
public void shouldMapFields() {
void shouldMapFields() {
ScmConfiguration config = createConfiguration();
when(subject.isPermitted("configuration:write:global")).thenReturn(true);
ConfigDto dto = mapper.map(config);
assertEquals("heartOfGold", dto.getProxyPassword());
assertEquals(1234, dto.getProxyPort());
assertEquals("proxyserver", dto.getProxyServer());
assertEquals("trillian", dto.getProxyUser());
assertTrue(dto.isEnableProxy());
assertEquals("description", dto.getRealmDescription());
assertTrue(dto.isDisableGroupingGrid());
assertEquals("dd", dto.getDateFormat());
assertSame(AnonymousMode.FULL, dto.getAnonymousMode());
assertEquals("baseurl", dto.getBaseUrl());
assertTrue(dto.isForceBaseUrl());
assertEquals(1, dto.getLoginAttemptLimit());
assertTrue("proxyExcludes", dto.getProxyExcludes().containsAll(Arrays.asList(expectedExcludes)));
assertTrue(dto.isSkipFailedAuthenticators());
assertEquals("pluginurl", dto.getPluginUrl());
assertEquals(2, dto.getLoginAttemptLimitTimeout());
assertTrue(dto.isEnabledXsrfProtection());
assertEquals("username", dto.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl());
assertEquals("https://www.scm-manager.org/download/rss.xml", dto.getReleaseFeedUrl());
assertEquals("scm-manager.local", dto.getMailDomainName());
assertTrue("emergencyContacts", dto.getEmergencyContacts().containsAll(Arrays.asList(expectedUsers)));
assertThat(dto.getProxyPassword()).isEqualTo("heartOfGold");
assertThat(dto.getProxyPort()).isEqualTo(1234);
assertThat(dto.getProxyServer()).isEqualTo("proxyserver");
assertThat(dto.getProxyUser()).isEqualTo("trillian");
assertThat(dto.isEnableProxy()).isTrue();
assertThat(dto.getRealmDescription()).isEqualTo("description");
assertThat(dto.isDisableGroupingGrid()).isTrue();
assertThat(dto.getDateFormat()).isEqualTo("dd");
assertThat(dto.getAnonymousMode()).isSameAs(AnonymousMode.FULL);
assertThat(dto.getBaseUrl()).isEqualTo("baseurl");
assertThat(dto.isForceBaseUrl()).isTrue();
assertThat(dto.getLoginAttemptLimit()).isOne();
assertThat(dto.getProxyExcludes()).contains(expectedExcludes);
assertThat(dto.isSkipFailedAuthenticators()).isTrue();
assertThat(dto.getPluginUrl()).isEqualTo("https://plug.ins");
assertThat(dto.getPluginAuthUrl()).isEqualTo("https://plug.ins/oidc");
assertThat(dto.getLoginAttemptLimitTimeout()).isEqualTo(2);
assertThat(dto.isEnabledXsrfProtection()).isTrue();
assertThat(dto.getNamespaceStrategy()).isEqualTo("username");
assertThat(dto.getLoginInfoUrl()).isEqualTo("https://scm-manager.org/login-info");
assertThat(dto.getReleaseFeedUrl()).isEqualTo("https://www.scm-manager.org/download/rss.xml");
assertThat(dto.getMailDomainName()).isEqualTo("scm-manager.local");
assertThat(dto.getEmergencyContacts()).contains(expectedUsers);
assertLinks(dto);
}
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
private void assertLinks(ConfigDto dto) {
assertThat(dto.getLinks().getLinkBy("self"))
.hasValueSatisfying(link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString()));
assertThat(dto.getLinks().getLinkBy("update"))
.hasValueSatisfying(link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString()));
}
@Test
public void shouldMapFieldsWithoutUpdate() {
void shouldMapFieldsWithoutUpdate() {
ScmConfiguration config = createConfiguration();
when(subject.hasRole("configuration:write:global")).thenReturn(false);
ConfigDto dto = mapper.map(config);
assertEquals("baseurl", dto.getBaseUrl());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
assertFalse(dto.getLinks().hasLink("update"));
assertThat(dto.getBaseUrl()).isEqualTo("baseurl");
assertThat(dto.getLinks().getLinkBy("self"))
.hasValueSatisfying(link -> assertThat(link.getHref()).isEqualTo(expectedBaseUri.toString()));
assertThat(dto.getLinks().hasLink("update")).isFalse();
}
@Test
public void shouldMapAnonymousAccessField() {
void shouldMapAnonymousAccessField() {
ScmConfiguration config = createConfiguration();
when(subject.hasRole("configuration:write:global")).thenReturn(false);
ConfigDto dto = mapper.map(config);
assertTrue(dto.isAnonymousAccessEnabled());
assertThat(dto.isAnonymousAccessEnabled()).isTrue();
config.setAnonymousMode(AnonymousMode.OFF);
ConfigDto secondDto = mapper.map(config);
assertFalse(secondDto.isAnonymousAccessEnabled());
assertThat(secondDto.isAnonymousAccessEnabled()).isFalse();
}
private ScmConfiguration createConfiguration() {
@@ -155,7 +163,8 @@ public class ScmConfigurationToConfigDtoMapperTest {
config.setLoginAttemptLimit(1);
config.setProxyExcludes(Sets.newSet(expectedExcludes));
config.setSkipFailedAuthenticators(true);
config.setPluginUrl("pluginurl");
config.setPluginUrl("https://plug.ins");
config.setPluginAuthUrl("https://plug.ins/oidc");
config.setLoginAttemptLimitTimeout(2);
config.setEnabledXsrfProtection(true);
config.setNamespaceStrategy("username");

View File

@@ -0,0 +1,302 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.plugin;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.apache.shiro.authz.AuthorizationException;
import org.github.sdorra.jse.ShiroExtension;
import org.github.sdorra.jse.SubjectAware;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequestWithBody;
import sonia.scm.net.ahc.AdvancedHttpResponse;
import sonia.scm.plugin.PluginCenterAuthenticator.RefreshResponse;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
import static sonia.scm.plugin.PluginCenterAuthenticator.*;
import static sonia.scm.plugin.PluginCenterAuthenticator.RefreshRequest;
@ExtendWith({MockitoExtension.class, ShiroExtension.class})
class PluginCenterAuthenticatorTest {
private PluginCenterAuthenticator authenticator;
@Mock
private AdvancedHttpClient advancedHttpClient;
@Mock(answer = Answers.RETURNS_SELF)
private AdvancedHttpRequestWithBody request;
private ScmConfiguration scmConfiguration;
@Mock
private ScmEventBus eventBus;
private final InMemoryConfigurationStoreFactory factory = InMemoryConfigurationStoreFactory.create();
@BeforeEach
void setUpObjectUnderTest() {
scmConfiguration = new ScmConfiguration();
authenticator = new PluginCenterAuthenticator(factory, scmConfiguration, advancedHttpClient, eventBus);
}
@Test
@SubjectAware("marvin")
void shouldFailAuthenticationWithoutPermissions() {
assertThrows(AuthorizationException.class, () -> authenticator.authenticate("marvin@hitchhiker.com", "refresh-token"));
}
@Test
@SubjectAware(value = "marvin", permissions = "plugin:read")
void shouldFailAuthenticationWithReadPermissions() {
assertThrows(AuthorizationException.class, () -> authenticator.authenticate("marvin@hitchhiker.com", "refresh-token"));
}
@Test
@SubjectAware("marvin")
void shouldFailToFetchAccessTokenWithoutPermission() {
assertThrows(AuthorizationException.class, () -> authenticator.fetchAccessToken());
}
@Test
@SubjectAware("marvin")
void shouldFailGetAuthenticationInfoWithoutPermission() {
assertThrows(AuthorizationException.class, () -> authenticator.getAuthenticationInfo());
}
@Test
@SubjectAware("marvin")
void shouldFailLogoutWithoutPermission() {
assertThrows(AuthorizationException.class, () -> authenticator.logout());
}
@Nested
@SubjectAware(value = "trillian", permissions = {"plugin:read", "plugin:write"})
class WithPermissions {
@Test
void shouldReturnFalseWithoutRefreshToken() {
assertThat(authenticator.isAuthenticated()).isFalse();
}
@Test
void shouldFailWithoutRefreshToken() {
assertThrows(IllegalArgumentException.class, () -> authenticator.authenticate("tricia.mcmillan@hitchhiker.com", null));
}
@Test
void shouldFailWithEmptyRefreshToken() {
assertThrows(IllegalArgumentException.class, () -> authenticator.authenticate("tricia.mcmillan@hitchhiker.com", ""));
}
@Test
void shouldFailWithoutSubject() {
assertThrows(IllegalArgumentException.class, () -> authenticator.authenticate(null, "rf"));
}
@Test
void shouldFailWithEmptySubject() {
assertThrows(IllegalArgumentException.class, () -> authenticator.authenticate("", "rf"));
}
@Test
void shouldFailWithoutPluginAuthUrl() {
scmConfiguration.setPluginAuthUrl(null);
assertThrows(IllegalStateException.class, () -> authenticator.authenticate("tricia.mcmillan@hitchhiker.com", "my-awesome-refresh-token"));
}
@Test
void shouldAuthenticate() throws IOException {
mockAuthProtocol("https://plugin-center-api.scm-manager.org/api/v1/auth/oidc/refresh", "access", "refresh");
authenticator.authenticate("tricia.mcmillan@hitchhiker.com", "my-awesome-refresh-token");
assertThat(authenticator.isAuthenticated()).isTrue();
}
@Test
void shouldFireLoginEvent() throws IOException {
mockAuthProtocol("https://plugin-center-api.scm-manager.org/api/v1/auth/oidc/refresh", "access", "refresh");
authenticator.authenticate("tricia.mcmillan@hitchhiker.com", "my-awesome-refresh-token");
ArgumentCaptor<PluginCenterLoginEvent> captor = ArgumentCaptor.forClass(PluginCenterLoginEvent.class);
verify(eventBus).post(captor.capture());
AuthenticationInfo info = captor.getValue().getAuthenticationInfo();
assertThat(info.getPluginCenterSubject()).isEqualTo("tricia.mcmillan@hitchhiker.com");
}
@Test
void shouldFailFetchWithoutPriorAuthentication() {
assertThrows(IllegalStateException.class, () -> authenticator.fetchAccessToken());
}
@Test
void shouldUseUrlFromScmConfiguration() throws IOException {
preAuth("cool-refresh-token");
scmConfiguration.setPluginAuthUrl("https://pca.org/oidc/");
mockAuthProtocol("https://pca.org/oidc/refresh", "access", "refresh");
String accessToken = authenticator.fetchAccessToken();
assertThat(accessToken).isEqualTo("access");
}
@Test
void shouldFailIfFetchFails() throws IOException {
preAuth("cool-refresh-token");
scmConfiguration.setPluginAuthUrl("https://plug.ins/oidc/");
when(advancedHttpClient.post("https://plug.ins/oidc/refresh")).thenReturn(request);
when(request.request()).thenThrow(new IOException("network down down down"));
assertThrows(FetchAccessTokenFailedException.class, () -> authenticator.fetchAccessToken());
}
@Test
void shouldFailIfFetchResponseIsNotSuccessful() throws IOException {
preAuth("cool-refresh-token");
scmConfiguration.setPluginAuthUrl("https://plug.ins/oidc/");
when(advancedHttpClient.post("https://plug.ins/oidc/refresh")).thenReturn(request);
AdvancedHttpResponse response = mock(AdvancedHttpResponse.class);
when(request.request()).thenReturn(response);
when(response.isSuccessful()).thenReturn(false);
assertThrows(FetchAccessTokenFailedException.class, () -> authenticator.fetchAccessToken());
}
@Test
void shouldFetchAccessToken() throws IOException {
preAuth("cool-refresh-token");
mockAuthProtocol("https://plugin-center-api.scm-manager.org/api/v1/auth/oidc/refresh", "access", "refresh");
String accessToken = authenticator.fetchAccessToken();
assertThat(accessToken).isEqualTo("access");
}
@Test
void shouldStoreRefreshTokenAfterFetch() throws IOException {
preAuth("refreshOne");
mockAuthProtocol("https://plugin-center-api.scm-manager.org/api/v1/auth/oidc/refresh", "accessTwo", "refreshTwo");
authenticator.fetchAccessToken();
authenticator.fetchAccessToken();
ArgumentCaptor<RefreshRequest> captor = ArgumentCaptor.forClass(RefreshRequest.class);
verify(request, times(2)).jsonContent(captor.capture());
List<String> refreshTokens = captor.getAllValues()
.stream()
.map(RefreshRequest::getRefreshToken)
.collect(Collectors.toList());
assertThat(refreshTokens).containsExactlyInAnyOrder("refreshOne", "refreshTwo");
}
@Test
void shouldReturnEmptyWithoutPriorAuthentication() {
assertThat(authenticator.getAuthenticationInfo()).isEmpty();
}
@Test
void shouldReturnAuthenticationInfo() {
preAuth("refresh_token");
assertThat(authenticator.getAuthenticationInfo()).hasValueSatisfying(info -> {
assertThat(info.getPluginCenterSubject()).isEqualTo("tricia.mcmillan@hitchhiker.com");
assertThat(info.getPrincipal()).isEqualTo("trillian");
assertThat(info.getDate()).isNotNull();
});
}
@Test
void shouldLogout() {
preAuth("refresh_token");
authenticator.logout();
assertThat(authenticator.isAuthenticated()).isFalse();
assertThat(authenticator.getAuthenticationInfo()).isEmpty();
}
@Test
void shouldFireLogoutEventAfterLogout() {
preAuth("refresh_token");
authenticator.logout();
ArgumentCaptor<PluginCenterLogoutEvent> captor = ArgumentCaptor.forClass(PluginCenterLogoutEvent.class);
verify(eventBus).post(captor.capture());
AuthenticationInfo info = captor.getValue().getPriorAuthenticationInfo();
assertThat(info.getPluginCenterSubject()).isEqualTo("tricia.mcmillan@hitchhiker.com");
}
@SuppressWarnings("unchecked")
private void preAuth(String refreshToken) {
Authentication authentication = new Authentication();
authentication.setPluginCenterSubject("tricia.mcmillan@hitchhiker.com");
authentication.setPrincipal("trillian");
authentication.setRefreshToken(refreshToken);
authentication.setDate(Instant.now());
factory.get(STORE_NAME, null).set(authentication);
}
@CanIgnoreReturnValue
private void mockAuthProtocol(String url, String accessToken, String refreshToken) throws IOException {
when(advancedHttpClient.post(url)).thenReturn(request);
AdvancedHttpResponse response = mock(AdvancedHttpResponse.class);
when(request.request()).thenReturn(response);
RefreshResponse refreshResponse = new RefreshResponse();
refreshResponse.setAccessToken(accessToken);
refreshResponse.setRefreshToken(refreshToken);
when(response.contentFromJson(RefreshResponse.class)).thenReturn(refreshResponse);
when(response.isSuccessful()).thenReturn(true);
}
}
}

View File

@@ -32,6 +32,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.event.ScmEventBus;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequest;
import sonia.scm.net.ahc.AdvancedHttpResponse;
import java.io.IOException;
@@ -40,8 +41,7 @@ import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@ExtendWith(MockitoExtension.class)
@@ -49,7 +49,7 @@ class PluginCenterLoaderTest {
private static final String PLUGIN_URL = "https://plugins.hitchhiker.com";
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@Mock
private AdvancedHttpClient client;
@Mock
@@ -58,9 +58,15 @@ class PluginCenterLoaderTest {
@Mock
private ScmEventBus eventBus;
@Mock
private PluginCenterAuthenticator authenticator;
@InjectMocks
private PluginCenterLoader loader;
@Mock(answer = Answers.RETURNS_SELF)
private AdvancedHttpRequest request;
@Test
void shouldFetch() throws IOException {
Set<AvailablePlugin> plugins = Collections.emptySet();
@@ -73,12 +79,16 @@ class PluginCenterLoaderTest {
}
private AdvancedHttpResponse request() throws IOException {
return client.get(PLUGIN_URL).spanKind(SPAN_KIND).request();
when(client.get(PLUGIN_URL)).thenReturn(request);
AdvancedHttpResponse response = mock(AdvancedHttpResponse.class);
when(request.request()).thenReturn(response);
return response;
}
@Test
void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException {
when(request()).thenThrow(new IOException("failed to fetch"));
when(client.get(PLUGIN_URL)).thenReturn(request);
when(request.request()).thenThrow(new IOException("failed to fetch"));
Set<AvailablePlugin> fetch = loader.load(PLUGIN_URL);
assertThat(fetch).isEmpty();
@@ -86,10 +96,31 @@ class PluginCenterLoaderTest {
@Test
void shouldFirePluginCenterErrorEvent() throws IOException {
when(request()).thenThrow(new IOException("failed to fetch"));
when(client.get(PLUGIN_URL)).thenReturn(request);
when(request.request()).thenThrow(new IOException("failed to fetch"));
loader.load(PLUGIN_URL);
verify(eventBus).post(any(PluginCenterErrorEvent.class));
}
@Test
void shouldAppendAccessToken() throws IOException {
when(authenticator.isAuthenticated()).thenReturn(true);
when(authenticator.fetchAccessToken()).thenReturn("mega-cool-at");
mockResponse();
loader.load(PLUGIN_URL);
verify(request).bearerAuth("mega-cool-at");
}
private Set<AvailablePlugin> mockResponse() throws IOException {
PluginCenterDto dto = new PluginCenterDto();
Set<AvailablePlugin> plugins = Collections.emptySet();
when(request().contentFromJson(PluginCenterDto.class)).thenReturn(dto);
when(mapper.map(dto)).thenReturn(plugins);
return plugins;
}
}

View File

@@ -45,6 +45,7 @@ import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -86,6 +87,7 @@ class PluginCenterTest {
}
@Test
@SuppressWarnings("unchecked")
void shouldCache() {
Set<AvailablePlugin> first = new HashSet<>();
when(loader.load(anyString())).thenReturn(first, new HashSet<>());
@@ -94,4 +96,25 @@ class PluginCenterTest {
assertThat(pluginCenter.getAvailable()).isSameAs(first);
}
@Test
@SuppressWarnings("unchecked")
void shouldClearCache() {
Set<AvailablePlugin> first = new HashSet<>();
when(loader.load(anyString())).thenReturn(first, new HashSet<>());
assertThat(pluginCenter.getAvailable()).isSameAs(first);
pluginCenter.handle(new PluginCenterLoginEvent(null));
assertThat(pluginCenter.getAvailable()).isNotSameAs(first);
}
@Test
void shouldLoadOnRefresh() {
Set<AvailablePlugin> plugins = new HashSet<>();
when(loader.load(PLUGIN_URL_BASE + "2.0.0")).thenReturn(plugins);
pluginCenter.refresh();
verify(loader).load(PLUGIN_URL_BASE + "2.0.0");
}
}

View File

@@ -34,6 +34,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequest;
import sonia.scm.net.ahc.AdvancedHttpResponse;
import java.io.ByteArrayInputStream;
@@ -46,28 +47,30 @@ import java.util.Collections;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
import static sonia.scm.plugin.Tracing.SPAN_KIND;
@ExtendWith({MockitoExtension.class})
@ExtendWith(MockitoExtension.class)
class PluginInstallerTest {
@Mock
private SCMContextProvider context;
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
@Mock
private AdvancedHttpClient client;
@Mock
private SmpDescriptorExtractor extractor;
@Mock
private PluginCenterAuthenticator authenticator;
@InjectMocks
private PluginInstaller installer;
@Mock(answer = Answers.RETURNS_SELF)
private AdvancedHttpRequest request;
private Path directory;
@BeforeEach
@@ -108,7 +111,10 @@ class PluginInstallerTest {
}
private AdvancedHttpResponse request(String url) throws IOException {
return client.get(url).spanKind(SPAN_KIND).request();
AdvancedHttpResponse response = mock(AdvancedHttpResponse.class);
when(client.get(url)).thenReturn(request);
when(request.request()).thenReturn(response);
return response;
}
private AvailablePlugin createGitPlugin() {
@@ -121,7 +127,8 @@ class PluginInstallerTest {
@Test
void shouldThrowPluginDownloadException() throws IOException {
when(request("https://download.hitchhiker.com")).thenThrow(new IOException("failed to download"));
when(client.get("https://download.hitchhiker.com")).thenReturn(request);
when(request.request()).thenThrow(new IOException("failed to download"));
PluginInstallationContext context = PluginInstallationContext.empty();
AvailablePlugin gitPlugin = createGitPlugin();
@@ -190,6 +197,17 @@ class PluginInstallerTest {
assertThat(exception.getDownloaded().getVersion()).isEqualTo("1.1.0");
}
@Test
void shouldAppendBearerAuth() throws IOException {
when(authenticator.isAuthenticated()).thenReturn(true);
when(authenticator.fetchAccessToken()).thenReturn("atat");
mockContent("42");
installer.install(PluginInstallationContext.empty(), createGitPlugin());
verify(request).bearerAuth("atat");
}
private AvailablePlugin createPlugin(String name, String url, String checksum) {
PluginInformation information = new PluginInformation();
information.setName(name);

View File

@@ -28,9 +28,7 @@ import org.mockito.Answers;
import java.util.Optional;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.*;
public class PluginTestHelper {
public static AvailablePlugin createAvailable(String name) {
@@ -62,9 +60,14 @@ public class PluginTestHelper {
}
public static AvailablePlugin createAvailable(PluginInformation information) {
return createAvailable(information, "https://scm-manager.org/download");
}
public static AvailablePlugin createAvailable(PluginInformation information, String url) {
AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class);
lenient().when(descriptor.getInformation()).thenReturn(information);
lenient().when(descriptor.getInstallLink()).thenReturn(Optional.of("mycloudogu.com/install/my_plugin"));
lenient().when(descriptor.getUrl()).thenReturn(url);
return new AvailablePlugin(descriptor);
}

View File

@@ -0,0 +1,65 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
class SecureParameterSerializerTest {
private final SecureParameterSerializer serializer = new SecureParameterSerializer(new ObjectMapper());
@Test
void shouldSerializeAndDeserialize() throws IOException {
TestObject object = new TestObject("1", 2);
String serialized = serializer.serialize(object);
assertThat(serialized).isNotEmpty();
object = serializer.deserialize(serialized, TestObject.class);
assertThat(object).isNotNull();
assertThat(object.getOne()).isEqualTo("1");
assertThat(object.getTwo()).isEqualTo(2);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class TestObject {
private String one;
private int two;
}
}

View File

@@ -30,17 +30,14 @@ import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EnumSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import java.util.Optional;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
@@ -58,6 +55,8 @@ class XsrfAccessTokenValidatorTest {
@Mock
private AccessToken accessToken;
private final XsrfExcludes excludes = new XsrfExcludes();
private XsrfAccessTokenValidator validator;
/**
@@ -65,7 +64,7 @@ class XsrfAccessTokenValidatorTest {
*/
@BeforeEach
void prepareObjectUnderTest() {
validator = new XsrfAccessTokenValidator(() -> request);
validator = new XsrfAccessTokenValidator(() -> request, excludes);
}
@Nested
@@ -86,7 +85,7 @@ class XsrfAccessTokenValidatorTest {
when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("abc");
// execute and assert
assertTrue(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isTrue();
}
/**
@@ -99,7 +98,7 @@ class XsrfAccessTokenValidatorTest {
when(request.getHeader(Xsrf.HEADER_KEY)).thenReturn("123");
// execute and assert
assertFalse(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isFalse();
}
/**
@@ -111,7 +110,7 @@ class XsrfAccessTokenValidatorTest {
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(Optional.of("abc"));
// execute and assert
assertFalse(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isFalse();
}
/**
@@ -123,30 +122,43 @@ class XsrfAccessTokenValidatorTest {
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(Optional.empty());
// execute and assert
assertTrue(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isTrue();
}
@Test
void shouldNotValidateExcludedRequest() {
excludes.add("/excluded");
// prepare
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(Optional.of("abc"));
when(request.getRequestURI()).thenReturn("/excluded");
// execute and assert
assertThat(validator.validate(accessToken)).isTrue();
}
}
@ParameterizedTest
@CsvSource({"GET", "HEAD", "OPTIONS"})
@ValueSource(strings = {"GET", "HEAD", "OPTIONS"})
void shouldNotValidateReadRequests(String method) {
// prepare
when(request.getMethod()).thenReturn(method);
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(Optional.of("abc"));
// execute and assert
assertTrue(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isTrue();
}
@ParameterizedTest
@CsvSource({"POST", "PUT", "DELETE", "PATCH"})
@ValueSource(strings = {"GET", "HEAD", "OPTIONS"})
void shouldFailValidationOfWriteRequests(String method) {
// prepare
when(request.getMethod()).thenReturn(method);
when(accessToken.getCustom(Xsrf.TOKEN_KEY)).thenReturn(Optional.of("abc"));
// execute and assert
assertFalse(validator.validate(accessToken));
assertThat(validator.validate(accessToken)).isTrue();
}
}