From 964c9d2c8de8de4246dd3621ca7c8720977790c2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 9 Oct 2019 11:38:30 +0200 Subject: [PATCH 01/27] implement anonymous access flag at administration/global-config on ui --- .../admin/components/form/GeneralSettings.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/scm-ui/src/admin/components/form/GeneralSettings.js b/scm-ui/src/admin/components/form/GeneralSettings.js index 91354badf1..d5d1150944 100644 --- a/scm-ui/src/admin/components/form/GeneralSettings.js +++ b/scm-ui/src/admin/components/form/GeneralSettings.js @@ -1,8 +1,8 @@ // @flow import React from "react"; -import { translate } from "react-i18next"; -import { Checkbox, InputField } from "@scm-manager/ui-components"; -import type { NamespaceStrategies } from "@scm-manager/ui-types"; +import {translate} from "react-i18next"; +import {Checkbox, InputField} from "@scm-manager/ui-components"; +import type {NamespaceStrategies} from "@scm-manager/ui-types"; import NamespaceStrategySelect from "./NamespaceStrategySelect"; type Props = { @@ -30,6 +30,7 @@ class GeneralSettings extends React.Component { loginInfoUrl, pluginUrl, enabledXsrfProtection, + anonymousAccessEnabled, namespaceStrategy, hasUpdatePermission, namespaceStrategies @@ -88,6 +89,15 @@ class GeneralSettings extends React.Component { helpText={t("help.pluginUrlHelpText")} /> +
+ +
); @@ -102,6 +112,9 @@ class GeneralSettings extends React.Component { handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; + handleEnableAnonymousAccess = (value: boolean) => { + this.props.onChange(true, value, "anonymousAccessEnabled"); + }; handleNamespaceStrategyChange = (value: string) => { this.props.onChange(true, value, "namespaceStrategy"); }; From 85562785335f45c7f90defb6c9eee82c82d47db1 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 9 Oct 2019 15:45:32 +0200 Subject: [PATCH 02/27] create _anonymous user when anonymous access activated and user does not exist yet / also create _anonymous user on system start if required --- .../scm/api/v2/resources/ConfigResource.java | 10 +++- .../scm/lifecycle/SetupContextListener.java | 12 ++++- .../api/v2/resources/ConfigResourceTest.java | 49 ++++++++++++++++++- .../lifecycle/SetupContextListenerTest.java | 48 +++++++++++++++++- ...fig-test-update-with-anonymous-access.json | 4 ++ 5 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index e5e754afea..73c144564b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -6,6 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; @@ -29,15 +31,17 @@ public class ConfigResource { private final ScmConfigurationToConfigDtoMapper configToDtoMapper; private final ScmConfiguration configuration; private final NamespaceStrategyValidator namespaceStrategyValidator; + private final UserManager userManager; @Inject public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, - ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator) { + ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator, UserManager userManager) { this.dtoToConfigMapper = dtoToConfigMapper; this.configToDtoMapper = configToDtoMapper; this.configuration = configuration; this.namespaceStrategyValidator = namespaceStrategyValidator; + this.userManager = userManager; } /** @@ -92,6 +96,10 @@ public class ConfigResource { ScmConfigurationUtil.getInstance().store(configuration); } + if (config.isAnonymousAccessEnabled() && !userManager.contains("_anonymous")) { + userManager.create(new User("_anonymous")); + } + return Response.noContent().build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java index bbc0dce120..69cb8a9224 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java @@ -4,6 +4,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.shiro.authc.credential.PasswordService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.config.ScmConfiguration; import sonia.scm.plugin.Extension; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; @@ -47,12 +48,14 @@ public class SetupContextListener implements ServletContextListener { private final UserManager userManager; private final PasswordService passwordService; private final PermissionAssigner permissionAssigner; + private final ScmConfiguration scmConfiguration; @Inject - public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner) { + public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner, ScmConfiguration scmConfiguration) { this.userManager = userManager; this.passwordService = passwordService; this.permissionAssigner = permissionAssigner; + this.scmConfiguration = scmConfiguration; } @Override @@ -60,6 +63,13 @@ public class SetupContextListener implements ServletContextListener { if (isFirstStart()) { createAdminAccount(); } + if (anonymousUserRequiredButNotExists()) { + userManager.create(new User("_anonymous")); + } + } + + private boolean anonymousUserRequiredButNotExists() { + return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains("_anonymous"); } private boolean isFirstStart() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 15c23dbd3c..eb86e0aa33 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -16,6 +16,8 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -29,7 +31,9 @@ import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @SubjectAware( @@ -50,6 +54,9 @@ public class ConfigResourceTest { @SuppressWarnings("unused") // Is injected private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private UserManager userManager; + @Mock private NamespaceStrategyValidator namespaceStrategyValidator; @@ -69,7 +76,7 @@ public class ConfigResourceTest { public void prepareEnvironment() { initMocks(this); - ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator); + ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator, userManager); dispatcher.getRegistry().addSingletonResource(configResource); } @@ -114,6 +121,45 @@ public class ConfigResourceTest { assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); } + @Test + @SubjectAware(username = "readWrite") + public void shouldUpdateConfigAndCreateAnonymousUser() throws URISyntaxException, IOException { + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); + response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); + assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); + assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); + verify(userManager).create(new User("_anonymous")); + } + + @Test + @SubjectAware(username = "readWrite") + public void shouldUpdateConfigAndNotCreateAnonymousUserIfAlreadyExists() throws URISyntaxException, IOException { + when(userManager.contains("_anonymous")).thenReturn(true); + MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); + + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + + request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); + response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); + assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); + assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); + verify(userManager, never()).create(new User("_anonymous")); + } + @Test @SubjectAware(username = "readOnly") public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { @@ -152,6 +198,7 @@ public class ConfigResourceTest { private static ScmConfiguration createConfiguration() { ScmConfiguration scmConfiguration = new ScmConfiguration(); scmConfiguration.setProxyPassword("heartOfGold"); + scmConfiguration.setAnonymousAccessEnabled(true); return scmConfiguration; } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java index 0a37361f9c..e518578607 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java @@ -11,6 +11,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import sonia.scm.config.ScmConfiguration; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; import sonia.scm.user.User; @@ -23,7 +24,12 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class SetupContextListenerTest { @@ -40,12 +46,20 @@ class SetupContextListenerTest { @Mock private PasswordService passwordService; + @Mock + ScmConfiguration scmConfiguration; + @Mock private PermissionAssigner permissionAssigner; @InjectMocks private SetupContextListener.SetupAction setupAction; + @BeforeEach + void mockScmConfiguration() { + when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(false); + } + @BeforeEach void setupObjectUnderTest() { doAnswer(ic -> { @@ -90,6 +104,38 @@ class SetupContextListenerTest { verify(permissionAssigner, never()).setPermissionsForUser(anyString(), any(Collection.class)); } + @Test + void shouldCreateAnonymousUserIfRequired() { + List users = Lists.newArrayList(UserTestData.createTrillian()); + when(userManager.getAll()).thenReturn(users); + when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true); + + setupContextListener.contextInitialized(null); + + verify(userManager).create(new User("_anonymous")); + } + + @Test + void shouldNotCreateAnonymousUserIfNotRequired() { + List users = Lists.newArrayList(UserTestData.createTrillian()); + when(userManager.getAll()).thenReturn(users); + + setupContextListener.contextInitialized(null); + + verify(userManager, never()).create(new User("_anonymous")); + } + + @Test + void shouldNotCreateAnonymousUserIfAlreadyExists() { + List users = Lists.newArrayList(new User("_anonymous")); + when(userManager.getAll()).thenReturn(users); + when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true); + + setupContextListener.contextInitialized(null); + + verify(userManager, times(1)).create(new User("_anonymous")); + } + private void verifyAdminPermissionsAssigned() { ArgumentCaptor usernameCaptor = ArgumentCaptor.forClass(String.class); ArgumentCaptor> permissionCaptor = ArgumentCaptor.forClass(Collection.class); diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json new file mode 100644 index 0000000000..7be259d272 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json @@ -0,0 +1,4 @@ +{ + "proxyPassword": "newPassword", + "anonymousAccessEnabled": "true" +} From 344ad696b26931de01354ad64764208ec3cec60f Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 9 Oct 2019 16:41:55 +0200 Subject: [PATCH 03/27] do not add _anonymous to group _authenticated / use AnonymousToken for anonymous access in authenticationFilter --- .../sonia/scm/security/AnonymousRealm.java | 34 +++++++++++++++++++ .../sonia/scm/security/AnonymousToken.java | 16 +++++++++ .../scm/web/filter/AuthenticationFilter.java | 15 ++++---- .../scm/group/DefaultGroupCollector.java | 5 ++- 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java create mode 100644 scm-core/src/main/java/sonia/scm/security/AnonymousToken.java diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java b/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java new file mode 100644 index 0000000000..836c19181b --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java @@ -0,0 +1,34 @@ +package sonia.scm.security; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.Inject; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.realm.AuthenticatingRealm; + +public class AnonymousRealm extends AuthenticatingRealm { + + /** + * realm name + */ + @VisibleForTesting + static final String REALM = "AnonymousRealm"; + + /** + * dao realm helper + */ + private final DAORealmHelper helper; + + @Inject + public AnonymousRealm(DAORealmHelperFactory helperFactory) { + this.helper = helperFactory.create(REALM); + + setAuthenticationTokenClass(AnonymousToken.class); + } + + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + return helper.authenticationInfoBuilder("_anonymous").build(); + } +} diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java b/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java new file mode 100644 index 0000000000..1712a04a75 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/AnonymousToken.java @@ -0,0 +1,16 @@ +package sonia.scm.security; + +import org.apache.shiro.authc.AuthenticationToken; + +public class AnonymousToken implements AuthenticationToken { + //Anonymous Token does not need an implementation + @Override + public Object getPrincipal() { + return null; + } + + @Override + public Object getCredentials() { + return null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java index c6a8463998..87209ce409 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/AuthenticationFilter.java @@ -37,31 +37,27 @@ package sonia.scm.web.filter; import com.google.inject.Inject; import com.google.inject.Singleton; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.subject.Subject; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; +import sonia.scm.security.AnonymousToken; import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.web.WebTokenGenerator; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; + +//~--- JDK imports ------------------------------------------------------------ /** * Handles authentication, if a one of the {@link WebTokenGenerator} returns @@ -134,6 +130,7 @@ public class AuthenticationFilter extends HttpFilter else if (isAnonymousAccessEnabled()) { logger.trace("anonymous access granted"); + subject.login(new AnonymousToken()); processChain(request, response, chain, subject); } else diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java index 8c6bac8103..072e975dbf 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -38,7 +38,10 @@ public class DefaultGroupCollector implements GroupCollector { public Set collect(String principal) { ImmutableSet.Builder builder = ImmutableSet.builder(); - builder.add(AUTHENTICATED); + if (principal != "_anonymous") { + builder.add(AUTHENTICATED); + } + builder.addAll(resolveExternalGroups(principal)); appendInternalGroups(principal, builder); From 18cb7020d014e41dd025ade7be0aec8c40617bdd Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 10 Oct 2019 13:40:40 +0200 Subject: [PATCH 04/27] implement anonymous realm // use constant for _anonymous user --- scm-core/src/main/java/sonia/scm/SCMContext.java | 2 +- .../main/java/sonia/scm/security/AnonymousRealm.java | 10 +++++++++- .../sonia/scm/api/v2/resources/ConfigResource.java | 6 +++--- .../java/sonia/scm/group/DefaultGroupCollector.java | 3 ++- .../java/sonia/scm/lifecycle/SetupContextListener.java | 5 +++-- .../sonia/scm/api/v2/resources/ConfigResourceTest.java | 8 ++++---- .../sonia/scm/lifecycle/SetupContextListenerTest.java | 9 +++++---- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/SCMContext.java b/scm-core/src/main/java/sonia/scm/SCMContext.java index 5af5d9f7d9..ce96412fef 100644 --- a/scm-core/src/main/java/sonia/scm/SCMContext.java +++ b/scm-core/src/main/java/sonia/scm/SCMContext.java @@ -51,7 +51,7 @@ public final class SCMContext public static final String DEFAULT_PACKAGE = "sonia.scm"; /** Name of the anonymous user */ - public static final String USER_ANONYMOUS = "anonymous"; + public static final String USER_ANONYMOUS = "_anonymous"; /** * the anonymous user diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java b/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java index 836c19181b..eb68c47027 100644 --- a/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java +++ b/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java @@ -5,8 +5,15 @@ import com.google.inject.Inject; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; import org.apache.shiro.realm.AuthenticatingRealm; +import sonia.scm.SCMContext; +import sonia.scm.plugin.Extension; +import javax.inject.Singleton; + +@Singleton +@Extension public class AnonymousRealm extends AuthenticatingRealm { /** @@ -25,10 +32,11 @@ public class AnonymousRealm extends AuthenticatingRealm { this.helper = helperFactory.create(REALM); setAuthenticationTokenClass(AnonymousToken.class); + setCredentialsMatcher(new AllowAllCredentialsMatcher()); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { - return helper.authenticationInfoBuilder("_anonymous").build(); + return helper.authenticationInfoBuilder(SCMContext.USER_ANONYMOUS).build(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index 73c144564b..71570909c9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -3,10 +3,10 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.SCMContext; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; -import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; @@ -96,8 +96,8 @@ public class ConfigResource { ScmConfigurationUtil.getInstance().store(configuration); } - if (config.isAnonymousAccessEnabled() && !userManager.contains("_anonymous")) { - userManager.create(new User("_anonymous")); + if (config.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { + userManager.create(SCMContext.ANONYMOUS); } return Response.noContent().build(); diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java index 072e975dbf..d1acccb364 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -4,6 +4,7 @@ import com.cronutils.utils.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; @@ -38,7 +39,7 @@ public class DefaultGroupCollector implements GroupCollector { public Set collect(String principal) { ImmutableSet.Builder builder = ImmutableSet.builder(); - if (principal != "_anonymous") { + if (!principal.equals(SCMContext.USER_ANONYMOUS)) { builder.add(AUTHENTICATED); } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java index 69cb8a9224..512a4fc534 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/SetupContextListener.java @@ -4,6 +4,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.shiro.authc.credential.PasswordService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.plugin.Extension; import sonia.scm.security.PermissionAssigner; @@ -64,12 +65,12 @@ public class SetupContextListener implements ServletContextListener { createAdminAccount(); } if (anonymousUserRequiredButNotExists()) { - userManager.create(new User("_anonymous")); + userManager.create(SCMContext.ANONYMOUS); } } private boolean anonymousUserRequiredButNotExists() { - return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains("_anonymous"); + return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS); } private boolean isFirstStart() { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index eb86e0aa33..8333cb14d1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -14,9 +14,9 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.InjectMocks; import org.mockito.Mock; +import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; -import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -137,13 +137,13 @@ public class ConfigResourceTest { assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - verify(userManager).create(new User("_anonymous")); + verify(userManager).create(SCMContext.ANONYMOUS); } @Test @SubjectAware(username = "readWrite") public void shouldUpdateConfigAndNotCreateAnonymousUserIfAlreadyExists() throws URISyntaxException, IOException { - when(userManager.contains("_anonymous")).thenReturn(true); + when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true); MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); MockHttpResponse response = new MockHttpResponse(); @@ -157,7 +157,7 @@ public class ConfigResourceTest { assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - verify(userManager, never()).create(new User("_anonymous")); + verify(userManager, never()).create(SCMContext.ANONYMOUS); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java index e518578607..27ddd42cc1 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/SetupContextListenerTest.java @@ -11,6 +11,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.security.PermissionAssigner; import sonia.scm.security.PermissionDescriptor; @@ -112,7 +113,7 @@ class SetupContextListenerTest { setupContextListener.contextInitialized(null); - verify(userManager).create(new User("_anonymous")); + verify(userManager).create(SCMContext.ANONYMOUS); } @Test @@ -122,18 +123,18 @@ class SetupContextListenerTest { setupContextListener.contextInitialized(null); - verify(userManager, never()).create(new User("_anonymous")); + verify(userManager, never()).create(SCMContext.ANONYMOUS); } @Test void shouldNotCreateAnonymousUserIfAlreadyExists() { - List users = Lists.newArrayList(new User("_anonymous")); + List users = Lists.newArrayList(SCMContext.ANONYMOUS); when(userManager.getAll()).thenReturn(users); when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true); setupContextListener.contextInitialized(null); - verify(userManager, times(1)).create(new User("_anonymous")); + verify(userManager, times(1)).create(SCMContext.ANONYMOUS); } private void verifyAdminPermissionsAssigned() { From f0311eb1384029abc7a380fac017585d249e8593 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 10 Oct 2019 13:44:29 +0200 Subject: [PATCH 05/27] fix authentication selector for correct browser rendering if anonymous access is active --- scm-ui/src/containers/App.js | 45 +++++-------------- scm-ui/src/containers/Login.js | 13 +++--- scm-ui/src/modules/auth.js | 19 ++++---- .../api/v2/resources/IndexDtoGenerator.java | 7 ++- 4 files changed, 31 insertions(+), 53 deletions(-) diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 1e1387fd70..e788fc84d8 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -1,25 +1,13 @@ // @flow -import React, { Component } from "react"; +import React, {Component} from "react"; import Main from "./Main"; -import { connect } from "react-redux"; -import { translate } from "react-i18next"; -import { withRouter } from "react-router-dom"; -import { - fetchMe, - isAuthenticated, - getMe, - isFetchMePending, - getFetchMeFailure -} from "../modules/auth"; +import {connect} from "react-redux"; +import {translate} from "react-i18next"; +import {withRouter} from "react-router-dom"; +import {fetchMe, getFetchMeFailure, getMe, isAuthenticated, isFetchMePending} from "../modules/auth"; -import { - PrimaryNavigation, - Loading, - ErrorPage, - Footer, - Header -} from "@scm-manager/ui-components"; -import type { Links, Me } from "@scm-manager/ui-types"; +import {ErrorPage, Footer, Header, Loading, PrimaryNavigation} from "@scm-manager/ui-components"; +import type {Links, Me} from "@scm-manager/ui-types"; import { getFetchIndexResourcesFailure, getLinks, @@ -50,23 +38,10 @@ class App extends Component { } render() { - const { - me, - loading, - error, - authenticated, - links, - t - } = this.props; + const {me, loading, error, authenticated, links, t} = this.props; let content; - const navigation = authenticated ? ( - - ) : ( - "" - ); + const navigation = authenticated ? : ""; if (loading) { content = ; @@ -85,7 +60,7 @@ class App extends Component {
{navigation}
{content} -
+ {authenticated &&
}
); } diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index f8246ab88b..b8c5b5ae9c 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -2,13 +2,13 @@ import React from "react"; import { Redirect, withRouter } from "react-router-dom"; import { - login, + getLoginFailure, isAuthenticated, isLoginPending, - getLoginFailure + login } from "../modules/auth"; import { connect } from "react-redux"; -import { getLoginLink, getLoginInfoLink } from "../modules/indexResource"; +import { getLoginInfoLink, getLoginLink } from "../modules/indexResource"; import LoginInfo from "../components/LoginInfo"; import classNames from "classnames"; import injectSheet from "react-jss"; @@ -37,7 +37,6 @@ type Props = { }; class Login extends React.Component { - handleLogin = (username: string, password: string): void => { const { link, login } = this.props; login(link, username, password); @@ -45,7 +44,7 @@ class Login extends React.Component { renderRedirect = () => { const { from } = this.props.location.state || { from: { pathname: "/" } }; - return ; + return ; }; render() { @@ -56,7 +55,7 @@ class Login extends React.Component { } return ( -
+
@@ -65,7 +64,7 @@ class Login extends React.Component {
- ); + ); } } diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index fdc3c83c7f..02b7b2396a 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -1,15 +1,16 @@ // @flow -import type { Me } from "@scm-manager/ui-types"; +import type {Me} from "@scm-manager/ui-types"; import * as types from "./types"; -import { apiClient, UnauthorizedError } from "@scm-manager/ui-components"; -import { isPending } from "./pending"; -import { getFailure } from "./failure"; +import {apiClient, UnauthorizedError} from "@scm-manager/ui-components"; +import {isPending} from "./pending"; +import {getFailure} from "./failure"; import { callFetchIndexResources, fetchIndexResources, fetchIndexResourcesPending, - fetchIndexResourcesSuccess + fetchIndexResourcesSuccess, + getLoginLink } from "./indexResource"; // Action @@ -44,13 +45,11 @@ export default function reducer( case FETCH_ME_SUCCESS: return { ...state, - me: action.payload, - authenticated: true + me: action.payload }; case FETCH_ME_UNAUTHORIZED: return { - me: {}, - authenticated: false + me: {} }; case LOGOUT_SUCCESS: return initialState; @@ -240,7 +239,7 @@ const stateAuth = (state: Object): Object => { }; export const isAuthenticated = (state: Object) => { - if (stateAuth(state).authenticated) { + if (state.auth.me && !getLoginLink(state)) { return true; } return false; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index b54c831662..70cdb93f33 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -6,12 +6,12 @@ import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; +import sonia.scm.SCMContext; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.PluginPermissions; -import sonia.scm.repository.RepositoryRolePermissions; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.UserPermissions; @@ -50,6 +50,11 @@ public class IndexDtoGenerator extends HalAppenderMapper { link("me", resourceLinks.me().self()), link("logout", resourceLinks.authentication().logout()) ); + + if (SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS)) { + builder.single(link("login", resourceLinks.authentication().jsonLogin())); + } + if (PluginPermissions.read().isPermitted()) { builder.single(link("installedPlugins", resourceLinks.installedPluginCollection().self())); builder.single(link("availablePlugins", resourceLinks.availablePluginCollection().self())); From 1162b353db3c68618819cd32c244c54f4a4f6cfa Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 10 Oct 2019 14:05:34 +0200 Subject: [PATCH 06/27] remove logout link when authenticated as _anonymous user --- scm-core/src/main/java/sonia/scm/SCMContext.java | 2 +- .../java/sonia/scm/api/v2/resources/IndexDtoGenerator.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/SCMContext.java b/scm-core/src/main/java/sonia/scm/SCMContext.java index ce96412fef..42d311ed2a 100644 --- a/scm-core/src/main/java/sonia/scm/SCMContext.java +++ b/scm-core/src/main/java/sonia/scm/SCMContext.java @@ -39,7 +39,7 @@ import sonia.scm.user.User; import sonia.scm.util.ServiceUtil; /** - * The SCMConext searches a implementation of {@link SCMContextProvider} and + * The SCMContext searches a implementation of {@link SCMContextProvider} and * holds a singleton instance of this implementation. * * @author Sebastian Sdorra diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 70cdb93f33..0e8552b6de 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -46,13 +46,12 @@ public class IndexDtoGenerator extends HalAppenderMapper { } if (SecurityUtils.getSubject().isAuthenticated()) { - builder.single( - link("me", resourceLinks.me().self()), - link("logout", resourceLinks.authentication().logout()) - ); + builder.single(link("me", resourceLinks.me().self())); if (SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS)) { builder.single(link("login", resourceLinks.authentication().jsonLogin())); + } else { + builder.single(link("logout", resourceLinks.authentication().logout())); } if (PluginPermissions.read().isPermitted()) { From 8d416c6c6721130e0c729ff557e9dbca956d296e Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 09:32:48 +0200 Subject: [PATCH 07/27] create ScmConfigurationChangedListener to create _anonymous user --- .../ScmConfigurationChangedListener.java | 29 +++++++++++++++++++ .../scm/api/v2/resources/ConfigResource.java | 5 ---- 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java new file mode 100644 index 0000000000..7460c29297 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java @@ -0,0 +1,29 @@ +package sonia.scm.config; + +import com.github.legman.Subscribe; +import com.google.inject.Inject; +import sonia.scm.EagerSingleton; +import sonia.scm.SCMContext; +import sonia.scm.plugin.Extension; +import sonia.scm.user.UserManager; + +@Extension +@EagerSingleton +public class ScmConfigurationChangedListener { + + private UserManager userManager; + + @Inject + public ScmConfigurationChangedListener(UserManager userManager) { + this.userManager = userManager; + } + + @Subscribe + public void handleEvent(ScmConfigurationChangedEvent event) { + if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { + userManager.create(SCMContext.ANONYMOUS); + } + } +} + + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index 71570909c9..6c568609ee 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; -import sonia.scm.SCMContext; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; @@ -96,10 +95,6 @@ public class ConfigResource { ScmConfigurationUtil.getInstance().store(configuration); } - if (config.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { - userManager.create(SCMContext.ANONYMOUS); - } - return Response.noContent().build(); } } From 1d79ed86ea4c341a608e91ff37e35b5dfbc128f2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 09:36:32 +0200 Subject: [PATCH 08/27] remove outdated unit test --- .../web/filter/AuthenticationFilterTest.java | 37 +++---------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java index d7d8a5d937..30b42fda21 100644 --- a/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java +++ b/scm-core/src/test/java/sonia/scm/web/filter/AuthenticationFilterTest.java @@ -37,33 +37,28 @@ package sonia.scm.web.filter; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; - import com.google.common.collect.ImmutableSet; - import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; - import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; - import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; - import sonia.scm.config.ScmConfiguration; import sonia.scm.web.WebTokenGenerator; -import static org.mockito.Mockito.*; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.verify; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -109,26 +104,6 @@ public class AuthenticationFilterTest "Authorization Required"); } - /** - * Method description - * - * - * @throws IOException - * @throws ServletException - */ - @Test - public void testDoFilterWithAnonymousAccess() - throws IOException, ServletException - { - configuration.setAnonymousAccessEnabled(true); - - AuthenticationFilter filter = createAuthenticationFilter(); - - filter.doFilter(request, response, chain); - verify(chain).doFilter(any(HttpServletRequest.class), - any(HttpServletResponse.class)); - } - /** * Method description * From eb684a3b1d62819832adf22dc7b224e1dda8bf28 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 10:18:26 +0200 Subject: [PATCH 09/27] only map authorizationException to forbidden if user is not _anonymous --- .../rest/AuthorizationExceptionMapper.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java index 18070b76df..2f00639a9b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java @@ -33,15 +33,19 @@ package sonia.scm.api.rest; //~--- non-JDK imports -------------------------------------------------------- +import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import sonia.scm.SCMContext; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; //~--- JDK imports ------------------------------------------------------------ -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.Provider; - /** * * @author Sebastian Sdorra @@ -49,20 +53,22 @@ import javax.ws.rs.ext.Provider; */ @Provider public class AuthorizationExceptionMapper - extends StatusExceptionMapper + implements ExceptionMapper { private static final Logger LOG = LoggerFactory.getLogger(AuthorizationExceptionMapper.class); - public AuthorizationExceptionMapper() - { - super(AuthorizationException.class, Response.Status.FORBIDDEN); - } - @Override public Response toResponse(AuthorizationException exception) { LOG.info("user is missing permission: {}", exception.getMessage()); - LOG.trace("AuthorizationException:", exception); - return super.toResponse(exception); + LOG.trace(getStatus().toString(), exception); + return Response.status(getStatus()) + .entity(exception.getMessage()) + .type(MediaType.TEXT_PLAIN_TYPE) + .build(); + } + + private Response.Status getStatus() { + return SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS) ? Response.Status.UNAUTHORIZED : Response.Status.FORBIDDEN; } } From adfaf6166c5196bb213114a1dfe35e3a2838b3a9 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 10:36:10 +0200 Subject: [PATCH 10/27] fix unit tests after removing authenticated flag --- scm-ui/src/modules/auth.test.js | 54 +++++++++++++++------------------ 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index cdf8e7c661..7f52fa4ae3 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -1,32 +1,35 @@ import reducer, { - fetchMeSuccess, - logout, - logoutSuccess, - loginSuccess, - fetchMeUnauthenticated, - LOGIN_SUCCESS, - login, - LOGIN_FAILURE, - LOGOUT_FAILURE, - LOGOUT_SUCCESS, - FETCH_ME_SUCCESS, - fetchMe, + FETCH_ME, FETCH_ME_FAILURE, - FETCH_ME_UNAUTHORIZED, - isAuthenticated, - LOGIN_PENDING, FETCH_ME_PENDING, - LOGOUT_PENDING, + FETCH_ME_SUCCESS, + FETCH_ME_UNAUTHORIZED, + fetchMe, + fetchMeSuccess, + fetchMeUnauthenticated, + getFetchMeFailure, + getLoginFailure, + getLogoutFailure, getMe, + isAuthenticated, isFetchMePending, isLoginPending, isLogoutPending, - getFetchMeFailure, + isRedirecting, + login, LOGIN, - FETCH_ME, + LOGIN_FAILURE, + LOGIN_PENDING, + LOGIN_SUCCESS, + loginSuccess, + logout, LOGOUT, - getLoginFailure, - getLogoutFailure, isRedirecting, LOGOUT_REDIRECT, redirectAfterLogout, + LOGOUT_FAILURE, + LOGOUT_PENDING, + LOGOUT_REDIRECT, + LOGOUT_SUCCESS, + logoutSuccess, + redirectAfterLogout } from "./auth"; import configureMockStore from "redux-mock-store"; @@ -47,22 +50,18 @@ describe("auth reducer", () => { it("should set me and login on successful fetch of me", () => { const state = reducer(undefined, fetchMeSuccess(me)); expect(state.me).toBe(me); - expect(state.authenticated).toBe(true); }); it("should set authenticated to false", () => { const initialState = { - authenticated: true, me }; const state = reducer(initialState, fetchMeUnauthenticated()); expect(state.me.name).toBeUndefined(); - expect(state.authenticated).toBe(false); }); it("should reset the state after logout", () => { const initialState = { - authenticated: true, me }; const state = reducer(initialState, logoutSuccess()); @@ -72,19 +71,16 @@ describe("auth reducer", () => { it("should keep state and set redirecting to true", () => { const initialState = { - authenticated: true, me }; const state = reducer(initialState, redirectAfterLogout()); expect(state.me).toBe(initialState.me); - expect(state.authenticated).toBe(initialState.authenticated); expect(state.redirecting).toBe(true); }); it("should set state authenticated and me after login", () => { const state = reducer(undefined, loginSuccess(me)); expect(state.me).toBe(me); - expect(state.authenticated).toBe(true); }); }); @@ -359,10 +355,10 @@ describe("auth selectors", () => { }); it("should return false, if redirecting is false", () => { - expect(isRedirecting({auth: { redirecting: false }})).toBe(false); + expect(isRedirecting({ auth: { redirecting: false } })).toBe(false); }); it("should return true, if redirecting is true", () => { - expect(isRedirecting({auth: { redirecting: true }})).toBe(true); + expect(isRedirecting({ auth: { redirecting: true } })).toBe(true); }); }); From ce2ea950a8d604dc91fa9c93ba6c6bbe20abf24b Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 11:12:29 +0200 Subject: [PATCH 11/27] fix and create unit tests for anonymous user --- scm-ui/src/modules/auth.test.js | 16 +--------------- .../api/v2/resources/ConfigResourceTest.java | 1 - .../RepositoryPermissionRootResourceTest.java | 17 +++++++++++++++++ .../java/sonia/scm/filter/MDCFilterTest.java | 3 ++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index 7f52fa4ae3..bda504721e 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -11,7 +11,6 @@ import reducer, { getLoginFailure, getLogoutFailure, getMe, - isAuthenticated, isFetchMePending, isLoginPending, isLogoutPending, @@ -35,10 +34,7 @@ import reducer, { import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; -import { - FETCH_INDEXRESOURCES_PENDING, - FETCH_INDEXRESOURCES_SUCCESS -} from "./indexResource"; +import {FETCH_INDEXRESOURCES_PENDING, FETCH_INDEXRESOURCES_SUCCESS} from "./indexResource"; const me = { name: "tricia", @@ -284,16 +280,6 @@ describe("auth actions", () => { describe("auth selectors", () => { const error = new Error("yo it failed"); - it("should be false, if authenticated is undefined or false", () => { - expect(isAuthenticated({})).toBe(false); - expect(isAuthenticated({ auth: {} })).toBe(false); - expect(isAuthenticated({ auth: { authenticated: false } })).toBe(false); - }); - - it("should be true, if authenticated is true", () => { - expect(isAuthenticated({ auth: { authenticated: true } })).toBe(true); - }); - it("should return me", () => { expect(getMe({ auth: { me } })).toBe(me); }); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 8333cb14d1..6b48360523 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -137,7 +137,6 @@ public class ConfigResourceTest { assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - verify(userManager).create(SCMContext.ANONYMOUS); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index e9ea0bead5..a1b6bd9e36 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; import com.google.inject.util.Providers; import de.otto.edison.hal.HalRepresentation; @@ -169,7 +170,9 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { @TestFactory @DisplayName("test endpoints on missing permissions and user is not Admin") + @SubjectAware(username = "trillian") Stream missedPermissionUserForbiddenTestFactory() { + when(subject.getPrincipal()).thenReturn("user"); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); return createDynamicTestsToAssertResponses( requestGETPermission.expectedResponseStatus(403), @@ -179,6 +182,20 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { requestPUTPermission.expectedResponseStatus(403)); } + @TestFactory + @DisplayName("test endpoints on missing permissions and user is not Admin") + @SubjectAware(username = "trillian") + Stream missedPermissionAnonymousUnauthorizedTestFactory() { + when(subject.getPrincipal()).thenReturn("_anonymous"); + doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); + return createDynamicTestsToAssertResponses( + requestGETPermission.expectedResponseStatus(401), + requestPOSTPermission.expectedResponseStatus(401), + requestGETAllPermissions.expectedResponseStatus(401), + requestDELETEPermission.expectedResponseStatus(401), + requestPUTPermission.expectedResponseStatus(401)); + } + @Test public void userWithPermissionWritePermissionShouldGetAllPermissionsWithCreateAndUpdateLinks() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); diff --git a/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java index efd3dbbbc0..ae7f452f96 100644 --- a/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/filter/MDCFilterTest.java @@ -40,6 +40,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.slf4j.MDC; import sonia.scm.AbstractTestBase; +import sonia.scm.SCMContext; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -117,7 +118,7 @@ public class MDCFilterTest extends AbstractTestBase { filter.doFilter(request, response, chain); assertNotNull(chain.ctx); - assertEquals("anonymous", chain.ctx.get(MDCFilter.MDC_USERNAME)); + assertEquals(SCMContext.USER_ANONYMOUS, chain.ctx.get(MDCFilter.MDC_USERNAME)); } private static class MDCCapturingFilterChain implements FilterChain { From 09409bae33f569d9451b9554f6623ba9944c5f79 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 11:34:32 +0200 Subject: [PATCH 12/27] cleanup --- .../sonia/scm/api/v2/resources/ConfigResource.java | 5 +---- .../scm/api/v2/resources/ConfigResourceTest.java | 11 +---------- .../RepositoryPermissionRootResourceTest.java | 3 --- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java index 6c568609ee..e5e754afea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ConfigResource.java @@ -6,7 +6,6 @@ import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; -import sonia.scm.user.UserManager; import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.VndMediaType; @@ -30,17 +29,15 @@ public class ConfigResource { private final ScmConfigurationToConfigDtoMapper configToDtoMapper; private final ScmConfiguration configuration; private final NamespaceStrategyValidator namespaceStrategyValidator; - private final UserManager userManager; @Inject public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, - ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator, UserManager userManager) { + ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator) { this.dtoToConfigMapper = dtoToConfigMapper; this.configToDtoMapper = configToDtoMapper; this.configuration = configuration; this.namespaceStrategyValidator = namespaceStrategyValidator; - this.userManager = userManager; } /** diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index 6b48360523..d274d7b6d7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -14,10 +14,8 @@ import org.junit.Test; import org.junit.rules.ExpectedException; import org.mockito.InjectMocks; import org.mockito.Mock; -import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.NamespaceStrategyValidator; -import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; @@ -31,9 +29,7 @@ import java.nio.charset.StandardCharsets; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @SubjectAware( @@ -54,9 +50,6 @@ public class ConfigResourceTest { @SuppressWarnings("unused") // Is injected private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - @Mock - private UserManager userManager; - @Mock private NamespaceStrategyValidator namespaceStrategyValidator; @@ -76,7 +69,7 @@ public class ConfigResourceTest { public void prepareEnvironment() { initMocks(this); - ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator, userManager); + ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator); dispatcher.getRegistry().addSingletonResource(configResource); } @@ -142,7 +135,6 @@ public class ConfigResourceTest { @Test @SubjectAware(username = "readWrite") public void shouldUpdateConfigAndNotCreateAnonymousUserIfAlreadyExists() throws URISyntaxException, IOException { - when(userManager.contains(SCMContext.USER_ANONYMOUS)).thenReturn(true); MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); MockHttpResponse response = new MockHttpResponse(); @@ -156,7 +148,6 @@ public class ConfigResourceTest { assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - verify(userManager, never()).create(SCMContext.ANONYMOUS); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index a1b6bd9e36..dcaf72dd56 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -2,7 +2,6 @@ package sonia.scm.api.v2.resources; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; import com.google.inject.util.Providers; import de.otto.edison.hal.HalRepresentation; @@ -170,7 +169,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { @TestFactory @DisplayName("test endpoints on missing permissions and user is not Admin") - @SubjectAware(username = "trillian") Stream missedPermissionUserForbiddenTestFactory() { when(subject.getPrincipal()).thenReturn("user"); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); @@ -184,7 +182,6 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { @TestFactory @DisplayName("test endpoints on missing permissions and user is not Admin") - @SubjectAware(username = "trillian") Stream missedPermissionAnonymousUnauthorizedTestFactory() { when(subject.getPrincipal()).thenReturn("_anonymous"); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); From b26f9068f40036e54ef29b7ac3addcb2545bbb99 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 16:18:14 +0200 Subject: [PATCH 13/27] move Anonymous Realm to webapp --- .../src/main/java/sonia/scm/security/AnonymousRealm.java | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {scm-core => scm-webapp}/src/main/java/sonia/scm/security/AnonymousRealm.java (100%) diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java b/scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java similarity index 100% rename from scm-core/src/main/java/sonia/scm/security/AnonymousRealm.java rename to scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java From 38ca5f8d22d319f0f1d0b9135561868d538c1d72 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 16:20:27 +0200 Subject: [PATCH 14/27] return 401 on scm http request if anonymous access is enabled but does not have the required permissions --- .../java/sonia/scm/web/protocol/HttpProtocolServlet.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java index 623be728c1..05eaac8d80 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -13,6 +13,8 @@ import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.security.Authentications; +import sonia.scm.util.HttpUtil; import sonia.scm.web.UserAgent; import sonia.scm.web.UserAgentParser; @@ -73,7 +75,11 @@ public class HttpProtocolServlet extends HttpServlet { resp.setStatus(HttpStatus.SC_NOT_FOUND); } catch (AuthorizationException e) { log.debug(e.getMessage()); - resp.setStatus(HttpStatus.SC_FORBIDDEN); + if (Authentications.isAuthenticatedSubjectAnonymous()) { + HttpUtil.sendUnauthorized(resp); + } else { + resp.setStatus(HttpStatus.SC_FORBIDDEN); + } } } } From d69e93406c6a6a7ae2635a4a5294fec40c2b62cc Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 14 Oct 2019 16:21:14 +0200 Subject: [PATCH 15/27] use static method to check if subject is anonymous --- .../java/sonia/scm/security/Authentications.java | 15 +++++++++++++++ .../api/rest/AuthorizationExceptionMapper.java | 5 ++--- .../scm/api/v2/resources/IndexDtoGenerator.java | 4 ++-- .../sonia/scm/group/DefaultGroupCollector.java | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 scm-core/src/main/java/sonia/scm/security/Authentications.java diff --git a/scm-core/src/main/java/sonia/scm/security/Authentications.java b/scm-core/src/main/java/sonia/scm/security/Authentications.java new file mode 100644 index 0000000000..dbd5fe6167 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/security/Authentications.java @@ -0,0 +1,15 @@ +package sonia.scm.security; + +import org.apache.shiro.SecurityUtils; +import sonia.scm.SCMContext; + +public class Authentications { + + public static boolean isAuthenticatedSubjectAnonymous() { + return SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS); + } + + public static boolean isSubjectAnonymous(String principal) { + return principal.equals(SCMContext.USER_ANONYMOUS); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java index 2f00639a9b..6bd9884d23 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/AuthorizationExceptionMapper.java @@ -33,11 +33,10 @@ package sonia.scm.api.rest; //~--- non-JDK imports -------------------------------------------------------- -import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.SCMContext; +import sonia.scm.security.Authentications; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @@ -69,6 +68,6 @@ public class AuthorizationExceptionMapper } private Response.Status getStatus() { - return SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS) ? Response.Status.UNAUTHORIZED : Response.Status.FORBIDDEN; + return Authentications.isAuthenticatedSubjectAnonymous() ? Response.Status.UNAUTHORIZED : Response.Status.FORBIDDEN; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index 0e8552b6de..6653bced14 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -6,12 +6,12 @@ import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.apache.shiro.SecurityUtils; -import sonia.scm.SCMContext; import sonia.scm.SCMContextProvider; import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ScmConfiguration; import sonia.scm.group.GroupPermissions; import sonia.scm.plugin.PluginPermissions; +import sonia.scm.security.Authentications; import sonia.scm.security.PermissionPermissions; import sonia.scm.user.UserPermissions; @@ -48,7 +48,7 @@ public class IndexDtoGenerator extends HalAppenderMapper { if (SecurityUtils.getSubject().isAuthenticated()) { builder.single(link("me", resourceLinks.me().self())); - if (SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS)) { + if (Authentications.isAuthenticatedSubjectAnonymous()) { builder.single(link("login", resourceLinks.authentication().jsonLogin())); } else { builder.single(link("logout", resourceLinks.authentication().logout())); diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java index d1acccb364..3aca4c4be6 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupCollector.java @@ -4,9 +4,9 @@ import com.cronutils.utils.VisibleForTesting; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.SCMContext; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; +import sonia.scm.security.Authentications; import javax.inject.Inject; import javax.inject.Singleton; @@ -39,7 +39,7 @@ public class DefaultGroupCollector implements GroupCollector { public Set collect(String principal) { ImmutableSet.Builder builder = ImmutableSet.builder(); - if (!principal.equals(SCMContext.USER_ANONYMOUS)) { + if (!Authentications.isSubjectAnonymous(principal)) { builder.add(AUTHENTICATED); } From 4ce1d2ed892aebacc39d05afb74e6fcfe3c43b04 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 09:26:36 +0200 Subject: [PATCH 16/27] add unit tests for anonymous realm --- .../sonia/scm/security/AnonymousRealm.java | 6 ++- .../scm/security/AnonymousRealmTest.java | 54 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/security/AnonymousRealmTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java b/scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java index eb68c47027..17a14cce73 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java +++ b/scm-webapp/src/main/java/sonia/scm/security/AnonymousRealm.java @@ -2,7 +2,6 @@ package sonia.scm.security; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; -import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher; @@ -12,6 +11,8 @@ import sonia.scm.plugin.Extension; import javax.inject.Singleton; +import static com.google.common.base.Preconditions.checkArgument; + @Singleton @Extension public class AnonymousRealm extends AuthenticatingRealm { @@ -36,7 +37,8 @@ public class AnonymousRealm extends AuthenticatingRealm { } @Override - protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) { + checkArgument(authenticationToken instanceof AnonymousToken, "%s is required", AnonymousToken.class); return helper.authenticationInfoBuilder(SCMContext.USER_ANONYMOUS).build(); } } diff --git a/scm-webapp/src/test/java/sonia/scm/security/AnonymousRealmTest.java b/scm-webapp/src/test/java/sonia/scm/security/AnonymousRealmTest.java new file mode 100644 index 0000000000..769d7406ca --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/security/AnonymousRealmTest.java @@ -0,0 +1,54 @@ +package sonia.scm.security; + +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.SCMContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class AnonymousRealmTest { + + @Mock + private DAORealmHelperFactory realmHelperFactory; + + @Mock + private DAORealmHelper realmHelper; + + @Mock + private DAORealmHelper.AuthenticationInfoBuilder builder; + + @InjectMocks + private AnonymousRealm realm; + + @Mock + private AuthenticationInfo authenticationInfo; + + @BeforeEach + void prepareObjectUnderTest() { + when(realmHelperFactory.create(AnonymousRealm.REALM)).thenReturn(realmHelper); + realm = new AnonymousRealm(realmHelperFactory); + } + + @Test + void shouldDoGetAuthentication() { + when(realmHelper.authenticationInfoBuilder(SCMContext.USER_ANONYMOUS)).thenReturn(builder); + when(builder.build()).thenReturn(authenticationInfo); + + AuthenticationInfo result = realm.doGetAuthenticationInfo(new AnonymousToken()); + assertThat(result).isSameAs(authenticationInfo); + } + + @Test + void shouldThrowIllegalArgumentExceptionForWrongTypeOfToken() { + assertThrows(IllegalArgumentException.class, () -> realm.doGetAuthenticationInfo(new UsernamePasswordToken())); + } +} From 63f773a206116f9cc5c84f2eaae37dca27038a0c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 09:28:15 +0200 Subject: [PATCH 17/27] add unit tests --- scm-ui/ui-webapp/src/modules/auth.test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scm-ui/ui-webapp/src/modules/auth.test.js b/scm-ui/ui-webapp/src/modules/auth.test.js index bda504721e..4ee2a9476c 100644 --- a/scm-ui/ui-webapp/src/modules/auth.test.js +++ b/scm-ui/ui-webapp/src/modules/auth.test.js @@ -11,6 +11,7 @@ import reducer, { getLoginFailure, getLogoutFailure, getMe, + isAuthenticated, isFetchMePending, isLoginPending, isLogoutPending, @@ -280,6 +281,21 @@ describe("auth actions", () => { describe("auth selectors", () => { const error = new Error("yo it failed"); + it("should return true if me exist and login Link does not exist", () => { + expect( + isAuthenticated({ auth: { me }, indexResources: { links: {} } }) + ).toBe(true); + }); + + it("should return false if me exist and login Link does exist", () => { + expect( + isAuthenticated({ + auth: { me }, + indexResources: { links: { login: { href: "login.href" } } } + }) + ).toBe(false); + }); + it("should return me", () => { expect(getMe({ auth: { me } })).toBe(me); }); From e91a3eac6a26f999adcc90b3e74355c709886a4c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 09:33:54 +0200 Subject: [PATCH 18/27] small fixes and cleanup after review --- .../ScmConfigurationChangedListener.java | 4 ++ .../sonia/scm/security/Authentications.java | 4 +- scm-ui/ui-webapp/src/containers/App.js | 6 +-- .../sonia/scm/user/DefaultUserManager.java | 5 --- .../api/v2/resources/ConfigResourceTest.java | 37 ------------------- ...fig-test-update-with-anonymous-access.json | 4 -- 6 files changed, 9 insertions(+), 51 deletions(-) delete mode 100644 scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java index 7460c29297..5cbda6d1e1 100644 --- a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java +++ b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java @@ -20,6 +20,10 @@ public class ScmConfigurationChangedListener { @Subscribe public void handleEvent(ScmConfigurationChangedEvent event) { + createAnonymousUserIfRequired(event); + } + + private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) { if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) { userManager.create(SCMContext.ANONYMOUS); } diff --git a/scm-core/src/main/java/sonia/scm/security/Authentications.java b/scm-core/src/main/java/sonia/scm/security/Authentications.java index dbd5fe6167..65332e20f4 100644 --- a/scm-core/src/main/java/sonia/scm/security/Authentications.java +++ b/scm-core/src/main/java/sonia/scm/security/Authentications.java @@ -6,10 +6,10 @@ import sonia.scm.SCMContext; public class Authentications { public static boolean isAuthenticatedSubjectAnonymous() { - return SecurityUtils.getSubject().getPrincipal().equals(SCMContext.USER_ANONYMOUS); + return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal()); } public static boolean isSubjectAnonymous(String principal) { - return principal.equals(SCMContext.USER_ANONYMOUS); + return SCMContext.USER_ANONYMOUS.equals(principal); } } diff --git a/scm-ui/ui-webapp/src/containers/App.js b/scm-ui/ui-webapp/src/containers/App.js index e788fc84d8..d227f83187 100644 --- a/scm-ui/ui-webapp/src/containers/App.js +++ b/scm-ui/ui-webapp/src/containers/App.js @@ -38,10 +38,10 @@ class App extends Component { } render() { - const {me, loading, error, authenticated, links, t} = this.props; + const { me, loading, error, authenticated, links, t } = this.props; let content; - const navigation = authenticated ? : ""; + const navigation = authenticated ? : ""; if (loading) { content = ; @@ -60,7 +60,7 @@ class App extends Component {
{navigation}
{content} - {authenticated &&
} + {authenticated &&
}
); } diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index b44db8d62a..212b067dbc 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -49,14 +49,9 @@ import sonia.scm.TransformFilter; import sonia.scm.search.SearchRequest; import sonia.scm.search.SearchUtil; import sonia.scm.util.CollectionAppender; -import sonia.scm.util.IOUtil; import sonia.scm.util.Util; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Unmarshaller; import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java index d274d7b6d7..15c23dbd3c 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ConfigResourceTest.java @@ -114,42 +114,6 @@ public class ConfigResourceTest { assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); } - @Test - @SubjectAware(username = "readWrite") - public void shouldUpdateConfigAndCreateAnonymousUser() throws URISyntaxException, IOException { - MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); - - MockHttpResponse response = new MockHttpResponse(); - dispatcher.invoke(request, response); - assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); - - request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); - response = new MockHttpResponse(); - dispatcher.invoke(request, response); - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); - assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); - assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - } - - @Test - @SubjectAware(username = "readWrite") - public void shouldUpdateConfigAndNotCreateAnonymousUserIfAlreadyExists() throws URISyntaxException, IOException { - MockHttpRequest request = post("sonia/scm/api/v2/config-test-update-with-anonymous-access.json"); - - MockHttpResponse response = new MockHttpResponse(); - dispatcher.invoke(request, response); - assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); - - request = MockHttpRequest.get("/" + ConfigResource.CONFIG_PATH_V2); - response = new MockHttpResponse(); - dispatcher.invoke(request, response); - assertEquals(HttpServletResponse.SC_OK, response.getStatus()); - assertTrue(response.getContentAsString().contains("\"proxyPassword\":\"newPassword\"")); - assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/config")); - assertTrue("link not found", response.getContentAsString().contains("\"update\":{\"href\":\"/v2/config")); - } - @Test @SubjectAware(username = "readOnly") public void shouldNotUpdateConfigWhenNotAuthorized() throws URISyntaxException, IOException { @@ -188,7 +152,6 @@ public class ConfigResourceTest { private static ScmConfiguration createConfiguration() { ScmConfiguration scmConfiguration = new ScmConfiguration(); scmConfiguration.setProxyPassword("heartOfGold"); - scmConfiguration.setAnonymousAccessEnabled(true); return scmConfiguration; } diff --git a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json b/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json deleted file mode 100644 index 7be259d272..0000000000 --- a/scm-webapp/src/test/resources/sonia/scm/api/v2/config-test-update-with-anonymous-access.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "proxyPassword": "newPassword", - "anonymousAccessEnabled": "true" -} From 0b2bbcb0da347bb4cdfd47dea435ad69be8eed55 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 09:36:06 +0200 Subject: [PATCH 19/27] update testfactory displayname --- .../api/v2/resources/RepositoryPermissionRootResourceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java index dcaf72dd56..f2b835a0a1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryPermissionRootResourceTest.java @@ -181,7 +181,7 @@ public class RepositoryPermissionRootResourceTest extends RepositoryTestBase { } @TestFactory - @DisplayName("test endpoints on missing permissions and user is not Admin") + @DisplayName("test endpoints on missing permissions and is _anonymous") Stream missedPermissionAnonymousUnauthorizedTestFactory() { when(subject.getPrincipal()).thenReturn("_anonymous"); doThrow(AuthorizationException.class).when(repositoryManager).get(any(NamespaceAndName.class)); From 2b98316dca6b744919db14d9b3d89265de169aa0 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 17 Oct 2019 09:53:02 +0200 Subject: [PATCH 20/27] Remove double import --- scm-ui/ui-webapp/src/containers/Login.js | 1 - 1 file changed, 1 deletion(-) diff --git a/scm-ui/ui-webapp/src/containers/Login.js b/scm-ui/ui-webapp/src/containers/Login.js index d4b691d2e9..fe1c68aa03 100644 --- a/scm-ui/ui-webapp/src/containers/Login.js +++ b/scm-ui/ui-webapp/src/containers/Login.js @@ -11,7 +11,6 @@ import { isLoginPending, getLoginFailure } from "../modules/auth"; -import { connect } from "react-redux"; import { getLoginInfoLink, getLoginLink } from "../modules/indexResource"; import LoginInfo from "../components/LoginInfo"; From c6cbd68e98fff2f8d7728cd8d09d54870fb61f6c Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 09:56:47 +0200 Subject: [PATCH 21/27] create unit tests for ScmConfigChangedListener --- .../ScmConfigurationChangedListenerTest.java | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java diff --git a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java new file mode 100644 index 0000000000..ab2a4f7f26 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java @@ -0,0 +1,60 @@ +package sonia.scm.config; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.user.UserManager; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class ScmConfigurationChangedListenerTest { + + @Mock + UserManager userManager; + + ScmConfiguration scmConfiguration = new ScmConfiguration(); + + @InjectMocks + ScmConfigurationChangedListener listener = new ScmConfigurationChangedListener(userManager); + + @Test + void shouldCreateAnonymousUserIfAnoymousAccessEnabled() { + when(userManager.contains(any())).thenReturn(false); + + ScmConfiguration changes = new ScmConfiguration(); + changes.setAnonymousAccessEnabled(true); + scmConfiguration.load(changes); + + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager).create(any()); + } + + @Test + void shouldNotCreateAnonymousUserIfAlreadyExists() { + when(userManager.contains(any())).thenReturn(true); + + ScmConfiguration changes = new ScmConfiguration(); + changes.setAnonymousAccessEnabled(true); + scmConfiguration.load(changes); + + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager, never()).create(any()); + } + + @Test + void shouldNotCreateAnonymousUserIfAnonymousAccessDisabled() { + ScmConfiguration changes = new ScmConfiguration(); + changes.setAnonymousAccessEnabled(false); + scmConfiguration.load(changes); + + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + verify(userManager, never()).create(any()); + } + +} From a33acf53265f689305ebac292e5340307c7bc208 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 10:45:10 +0200 Subject: [PATCH 22/27] update anonymous access help text --- scm-ui/ui-webapp/public/locales/de/config.json | 2 +- scm-ui/ui-webapp/public/locales/en/config.json | 2 +- scm-ui/ui-webapp/public/locales/es/config.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/config.json b/scm-ui/ui-webapp/public/locales/de/config.json index 5f598cebbe..4cbfff2afc 100644 --- a/scm-ui/ui-webapp/public/locales/de/config.json +++ b/scm-ui/ui-webapp/public/locales/de/config.json @@ -57,7 +57,7 @@ "pluginUrlHelpText": "Die URL der Plugin Center API. Beschreibung der Platzhalter: version = SCM-Manager Version; os = Betriebssystem; arch = Architektur", "enableForwardingHelpText": "mod_proxy Port Weiterleitung aktivieren.", "disableGroupingGridHelpText": "Repository Gruppen deaktivieren. Nach einer Änderung an dieser Einstellung muss die Seite komplett neu geladen werden.", - "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf öffentliche Repositories.", + "allowAnonymousAccessHelpText": "Anonyme Benutzer haben Zugriff auf freigegebene Repositories.", "skipFailedAuthenticatorsHelpText": "Die Kette der Authentifikatoren wird nicht beendet, wenn ein Authentifikator einen Benutzer findet, ihn aber nicht erfolgreich authentifizieren kann.", "adminGroupsHelpText": "Namen von Gruppen mit Admin-Berechtigungen.", "adminUsersHelpText": "Namen von Benutzern mit Admin-Berechtigungen.", diff --git a/scm-ui/ui-webapp/public/locales/en/config.json b/scm-ui/ui-webapp/public/locales/en/config.json index 6b602a17be..a3720cd67c 100644 --- a/scm-ui/ui-webapp/public/locales/en/config.json +++ b/scm-ui/ui-webapp/public/locales/en/config.json @@ -57,7 +57,7 @@ "pluginUrlHelpText": "The url of the Plugin Center API. Explanation of the placeholders: version = SCM-Manager Version; os = Operation System; arch = Architecture", "enableForwardingHelpText": "Enable mod_proxy port forwarding.", "disableGroupingGridHelpText": "Disable repository Groups. A complete page reload is required after a change of this value.", - "allowAnonymousAccessHelpText": "Anonymous users have read access on public repositories.", + "allowAnonymousAccessHelpText": "Anonymous users have access on granted repositories.", "skipFailedAuthenticatorsHelpText": "Do not stop the authentication chain, if an authenticator finds the user but fails to authenticate the user.", "adminGroupsHelpText": "Names of groups with admin permissions.", "adminUsersHelpText": "Names of users with admin permissions.", diff --git a/scm-ui/ui-webapp/public/locales/es/config.json b/scm-ui/ui-webapp/public/locales/es/config.json index 3b5ab843dd..8357e72546 100644 --- a/scm-ui/ui-webapp/public/locales/es/config.json +++ b/scm-ui/ui-webapp/public/locales/es/config.json @@ -57,7 +57,7 @@ "pluginUrlHelpText": "La URL de la API del almacén de complementos. Explicación de los marcadores: version = Versión de SCM-Manager; os = Sistema operativo; arch = Arquitectura", "enableForwardingHelpText": "Habilitar el redireccionamiento de puertos para mod_proxy.", "disableGroupingGridHelpText": "Deshabilitar los grupos de repositorios. Se requiere una recarga completa de la página después de un cambio en este valor.", - "allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso de lectura en los repositorios públicos.", + "allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso a repositorios otorgados.", "skipFailedAuthenticatorsHelpText": "No detenga la cadena de autenticación si un autenticador encuentra al usuario pero no puede autenticarlo.", "adminGroupsHelpText": "Nombres de los grupos con permisos de administrador.", "adminUsersHelpText": "Nombres de los usuarios con permisos de administrador.", From 1fd6337f64036c68c19eb0f3120b73d018ece978 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 11:08:55 +0200 Subject: [PATCH 23/27] anonymous user should not have permission to change password or autocomplete --- .../scm/api/v2/resources/MeDtoFactory.java | 3 ++- .../DefaultAuthorizationCollector.java | 8 +++++--- .../sonia/scm/user/DefaultUserManager.java | 9 +++++++-- .../scm/api/v2/resources/MeDtoFactoryTest.java | 15 +++++++++++++++ .../DefaultAuthorizationCollectorTest.java | 18 ++++++++++++++++++ .../src/test/resources/sonia/scm/shiro-001.ini | 1 + 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java index c2bebd389a..34bd035c6f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeDtoFactory.java @@ -6,6 +6,7 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import sonia.scm.group.GroupCollector; +import sonia.scm.security.Authentications; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; @@ -63,7 +64,7 @@ public class MeDtoFactory extends HalAppenderMapper { if (UserPermissions.modify(user).isPermitted()) { linksBuilder.single(link("update", resourceLinks.me().update(user.getName()))); } - if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) { + if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) { linksBuilder.single(link("password", resourceLinks.me().passwordChange())); } diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 28f61df34f..57866ebe3c 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -254,9 +254,11 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector collectGlobalPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups); builder.add(canReadOwnUser(user)); - builder.add(getUserAutocompletePermission()); - builder.add(getGroupAutocompletePermission()); - builder.add(getChangeOwnPasswordPermission(user)); + if (!Authentications.isSubjectAnonymous(user.getName())) { + builder.add(getUserAutocompletePermission()); + builder.add(getGroupAutocompletePermission()); + builder.add(getChangeOwnPasswordPermission(user)); + } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(ImmutableSet.of(Role.USER)); info.addStringPermissions(builder.build()); diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index 212b067dbc..d297d51b45 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -48,6 +48,7 @@ import sonia.scm.SCMContextProvider; import sonia.scm.TransformFilter; import sonia.scm.search.SearchRequest; import sonia.scm.search.SearchUtil; +import sonia.scm.security.Authentications; import sonia.scm.util.CollectionAppender; import sonia.scm.util.Util; @@ -378,7 +379,7 @@ public class DefaultUserManager extends AbstractUserManager public void changePasswordForLoggedInUser(String oldPassword, String newPassword) { User user = get((String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal()); - if (!user.getPassword().equals(oldPassword)) { + if (!isAnonymousUser(user) && !user.getPassword().equals(oldPassword)) { throw new InvalidPasswordException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName())); } @@ -397,13 +398,17 @@ public class DefaultUserManager extends AbstractUserManager if (user == null) { throw new NotFoundException(User.class, userId); } - if (!isTypeDefault(user)) { + if (!isTypeDefault(user) || isAnonymousUser(user)) { throw new ChangePasswordNotAllowedException(ContextEntry.ContextBuilder.entity("PasswordChange", "-").in(User.class, user.getName()), user.getType()); } user.setPassword(newPassword); this.modify(user); } + private boolean isAnonymousUser(User user) { + return Authentications.isSubjectAnonymous(user.getName()); + } + //~--- fields --------------------------------------------------------------- private final UserDAO userDAO; diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java index d9572dc04c..42d78b99f1 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeDtoFactoryTest.java @@ -12,14 +12,17 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import sonia.scm.SCMContext; import sonia.scm.group.GroupCollector; import sonia.scm.user.User; import sonia.scm.user.UserManager; +import sonia.scm.user.UserPermissions; import sonia.scm.user.UserTestData; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -159,6 +162,18 @@ class MeDtoFactoryTest { assertThat(dto.getLinks().getLinkBy("password")).isNotPresent(); } + @Test + void shouldNotGetPasswordLinkForAnonymousUser() { + User user = SCMContext.ANONYMOUS; + prepareSubject(user); + + when(userManager.isTypeDefault(any())).thenReturn(true); + when(UserPermissions.changePassword(user).isPermitted()).thenReturn(true); + + MeDto dto = meDtoFactory.create(); + assertThat(dto.getLinks().getLinkBy("password")).isNotPresent(); + } + @Test void shouldAppendLinks() { prepareSubject(UserTestData.createTrillian()); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 930a06d249..08216e9fd2 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -48,6 +48,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.SCMContext; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.group.GroupCollector; @@ -172,6 +173,23 @@ public class DefaultAuthorizationCollectorTest { assertThat(authInfo.getObjectPermissions(), nullValue()); } + /** + * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} without permissions. + */ + @Test + @SubjectAware( + configuration = "classpath:sonia/scm/shiro-001.ini" + ) + public void testCollectWithoutPermissionsForAnonymousUser() { + User anonymous = SCMContext.ANONYMOUS; + authenticate(anonymous, "anon"); + + AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getStringPermissions(), hasSize(1)); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:read:_anonymous")); + assertThat(authInfo.getObjectPermissions(), nullValue()); + } + /** * Tests {@link AuthorizationCollector#collect(PrincipalCollection)} ()} with repository permissions. */ diff --git a/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini index 54741bcf4d..df0fd4940c 100644 --- a/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini +++ b/scm-webapp/src/test/resources/sonia/scm/shiro-001.ini @@ -1,6 +1,7 @@ [users] trillian = secret, user dent = secret, admin +_anonymous = secret, user [roles] admin = * From d9cf93b59abff05688c4a2ff6e114c564d463c26 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 17 Oct 2019 11:17:33 +0200 Subject: [PATCH 24/27] cleanup --- .../scm/config/ScmConfigurationChangedListenerTest.java | 8 ++------ .../scm/security/DefaultAuthorizationCollectorTest.java | 4 ++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java index ab2a4f7f26..67a275c0ad 100644 --- a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java +++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java @@ -49,12 +49,8 @@ class ScmConfigurationChangedListenerTest { @Test void shouldNotCreateAnonymousUserIfAnonymousAccessDisabled() { - ScmConfiguration changes = new ScmConfiguration(); - changes.setAnonymousAccessEnabled(false); - scmConfiguration.load(changes); - - listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); + //anonymous access disabled by default + listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration)); verify(userManager, never()).create(any()); } - } diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index 08216e9fd2..e15702daca 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -181,10 +181,10 @@ public class DefaultAuthorizationCollectorTest { configuration = "classpath:sonia/scm/shiro-001.ini" ) public void testCollectWithoutPermissionsForAnonymousUser() { - User anonymous = SCMContext.ANONYMOUS; - authenticate(anonymous, "anon"); + authenticate(SCMContext.ANONYMOUS, "anon"); AuthorizationInfo authInfo = collector.collect(); + assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); assertThat(authInfo.getStringPermissions(), hasSize(1)); assertThat(authInfo.getStringPermissions(), containsInAnyOrder("user:read:_anonymous")); assertThat(authInfo.getObjectPermissions(), nullValue()); From 20338c1bb6733c6ce4040059906f96d7177eecc9 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 17 Oct 2019 11:53:23 +0200 Subject: [PATCH 25/27] Add integration tests for anonymous access --- .../sonia/scm/it/AnonymousAccessITCase.java | 154 +++++++++++++++++- .../sonia/scm/it/utils/RepositoryUtil.java | 8 + .../java/sonia/scm/it/utils/RestUtil.java | 10 +- .../java/sonia/scm/it/utils/ScmTypes.java | 11 +- .../java/sonia/scm/it/utils/TestData.java | 13 +- .../client/api/RepositoryClientFactory.java | 6 + 6 files changed, 192 insertions(+), 10 deletions(-) diff --git a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java index 0a329b8e1e..1b48da792b 100644 --- a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java @@ -1,26 +1,170 @@ package sonia.scm.it; import io.restassured.RestAssured; -import org.junit.Test; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.junitpioneer.jupiter.TempDirectory; +import sonia.scm.it.utils.RepositoryUtil; import sonia.scm.it.utils.RestUtil; import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.ScmTypes; +import sonia.scm.it.utils.TestData; +import sonia.scm.repository.client.api.RepositoryClient; +import sonia.scm.repository.client.api.RepositoryClientException; +import javax.json.Json; +import javax.json.JsonArray; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import static java.util.Collections.emptyMap; import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static sonia.scm.it.utils.TestData.JSON_BUILDER; +import static sonia.scm.it.utils.TestData.USER_ANONYMOUS; +import static sonia.scm.it.utils.TestData.WRITE; +import static sonia.scm.it.utils.TestData.getDefaultRepositoryUrl; -public class AnonymousAccessITCase { +@ExtendWith(TempDirectory.class) +class AnonymousAccessITCase { @Test - public void shouldAccessIndexResourceWithoutAuthentication() { + void shouldAccessIndexResourceWithoutAuthentication() { ScmRequests.start() .requestIndexResource() .assertStatusCode(200); } @Test - public void shouldRejectUserResourceWithoutAuthentication() { + void shouldRejectRepositoryResourceWithoutAuthentication() { assertEquals(401, RestAssured.given() .when() - .get(RestUtil.REST_BASE_URL.resolve("users/")) + .get(RestUtil.REST_BASE_URL.resolve("repositories/")) .statusCode()); } + + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class WithAnonymousAccess { + @BeforeAll + void enableAnonymousAccess() { + setAnonymousAccess(true); + } + + @BeforeEach + void createRepository() { + TestData.createDefault(); + } + + @Test + void shouldGrantAnonymousAccessToRepositoryList() { + assertEquals(200, RestAssured.given() + .when() + .get(RestUtil.REST_BASE_URL.resolve("repositories")) + .statusCode()); + } + + @Nested + class WithoutAnonymousAccessForRepository { + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldGrantAnonymousAccessToRepository(String type) { + assertEquals(401, RestAssured.given() + .when() + .get(getDefaultRepositoryUrl(type)) + .statusCode()); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldNotCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) { + assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile())); + } + } + + @Nested + class WithAnonymousAccessForRepository { + + @BeforeEach + void grantAnonymousAccessToRepo() { + ScmTypes.availableScmTypes().stream().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type)); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldGrantAnonymousAccessToRepository(String type) { + assertEquals(200, RestAssured.given() + .when() + .get(getDefaultRepositoryUrl(type)) + .statusCode()); + } + + @ParameterizedTest + @ArgumentsSource(ScmTypes.class) + void shouldCloneRepository(String type, @TempDirectory.TempDir Path temporaryFolder) throws IOException { + RepositoryClient client = RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()); + assertEquals(1, Objects.requireNonNull(client.getWorkingCopy().list()).length); + } + } + + @AfterAll + void disableAnonymousAccess() { + setAnonymousAccess(false); + } + } + + private static void setAnonymousAccess(boolean anonymousAccessEnabled) { + RestUtil.given("application/vnd.scmm-config+json;v=2") + .body(createConfig(anonymousAccessEnabled)) + + .when() + .put(RestUtil.REST_BASE_URL.toASCIIString() + "config") + + .then() + .statusCode(HttpServletResponse.SC_NO_CONTENT); + } + + private static String createConfig(boolean anonymousAccessEnabled) { + JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build(); + return JSON_BUILDER + .add("adminGroups", emptyArray) + .add("adminUsers", emptyArray) + .add("anonymousAccessEnabled", anonymousAccessEnabled) + .add("baseUrl", "https://next-scm.cloudogu.com/scm") + .add("dateFormat", "YYYY-MM-DD HH:mm:ss") + .add("disableGroupingGrid", false) + .add("enableProxy", false) + .add("enabledXsrfProtection", true) + .add("forceBaseUrl", false) + .add("loginAttemptLimit", 100) + .add("loginAttemptLimitTimeout", 300) + .add("loginInfoUrl", "https://login-info.scm-manager.org/api/v1/login-info") + .add("namespaceStrategy", "UsernameNamespaceStrategy") + .add("pluginUrl", "https://oss.cloudogu.com/jenkins/job/scm-manager/job/scm-manager-bitbucket/job/plugin-snapshot/job/master/lastSuccessfulBuild/artifact/plugins/plugin-center.json") + .add("proxyExcludes", emptyArray) + .addNull("proxyPassword") + .add("proxyPort", 8080) + .add("proxyServer", "proxy.mydomain.com") + .addNull("proxyUser") + .add("realmDescription", "SONIA :: SCM Manager") + .add("skipFailedAuthenticators", false) + .build().toString(); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java index 427d98f245..752be88dbc 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java @@ -36,6 +36,14 @@ public class RepositoryUtil { return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); } + public static RepositoryClient createAnonymousRepositoryClient(String repositoryType, File folder) throws IOException { + String httpProtocolUrl = TestData.callRepository("scmadmin", "scmadmin", repositoryType, HttpStatus.SC_OK) + .extract() + .path("_links.protocol.find{it.name=='http'}.href"); + + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, folder); + } + public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException { String uuid = UUID.randomUUID().toString(); String name = "file-" + uuid + ".uuid"; diff --git a/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java index 645cf06ac8..34c28a2a46 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java @@ -29,9 +29,13 @@ public class RestUtil { } public static RequestSpecification given(String mediaType, String username, String password) { - return RestAssured.given() - .contentType(mediaType) - .accept(mediaType) + return givenAnonymous(mediaType) .auth().preemptive().basic(username, password); } + + public static RequestSpecification givenAnonymous(String mediaType) { + return RestAssured.given() + .contentType(mediaType) + .accept(mediaType); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java index 4c9ac0ea44..4b48c89bbc 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java @@ -1,11 +1,15 @@ package sonia.scm.it.utils; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; import sonia.scm.util.IOUtil; import java.util.ArrayList; import java.util.Collection; +import java.util.stream.Stream; -public class ScmTypes { +public class ScmTypes implements ArgumentsProvider { public static Collection availableScmTypes() { Collection params = new ArrayList<>(); @@ -18,4 +22,9 @@ public class ScmTypes { return params; } + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return availableScmTypes().stream().map(Arguments::of); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java index c7d97a6891..cfefc1171a 100644 --- a/scm-it/src/test/java/sonia/scm/it/utils/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -18,6 +18,7 @@ import java.util.stream.Collectors; import static java.util.Arrays.asList; import static sonia.scm.it.utils.RestUtil.createResourceUrl; import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.RestUtil.givenAnonymous; import static sonia.scm.it.utils.ScmTypes.availableScmTypes; public class TestData { @@ -25,7 +26,7 @@ public class TestData { private static final Logger LOG = LoggerFactory.getLogger(TestData.class); public static final String USER_SCM_ADMIN = "scmadmin"; - public static final String USER_ANONYMOUS = "anonymous"; + public static final String USER_ANONYMOUS = "_anonymous"; public static final Collection READ = asList("read", "pull"); public static final Collection WRITE = asList("read", "write", "pull", "push"); @@ -147,6 +148,16 @@ public class TestData { .statusCode(expectedStatusCode); } + public static ValidatableResponse callAnonymousRepository(String repositoryType, int expectedStatusCode) { + return givenAnonymous(VndMediaType.REPOSITORY) + + .when() + .get(getDefaultRepositoryUrl(repositoryType)) + + .then() + .statusCode(expectedStatusCode); + } + public static String getDefaultPermissionUrl(String username, String password, String repositoryType) { return given(VndMediaType.REPOSITORY, username, password) .when() diff --git a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java index 17f1818e33..8dec6ec524 100644 --- a/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java +++ b/scm-test/src/main/java/sonia/scm/repository/client/api/RepositoryClientFactory.java @@ -123,6 +123,12 @@ public final class RepositoryClientFactory password, workingCopy)); } + public RepositoryClient create(String type, String url, File workingCopy) + throws IOException + { + return new RepositoryClient(getProvider(type).create(url, null, null, workingCopy)); + } + //~--- get methods ---------------------------------------------------------- /** From b2a3bd183c5cb5d2caf9080fe88a901af1a0e295 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 17 Oct 2019 12:08:39 +0200 Subject: [PATCH 26/27] Use formatting from prettier --- scm-ui/ui-webapp/src/containers/App.js | 26 +++++++++++++++++------ scm-ui/ui-webapp/src/containers/Login.js | 17 +++++++++------ scm-ui/ui-webapp/src/modules/auth.js | 8 +++---- scm-ui/ui-webapp/src/modules/auth.test.js | 5 ++++- 4 files changed, 38 insertions(+), 18 deletions(-) diff --git a/scm-ui/ui-webapp/src/containers/App.js b/scm-ui/ui-webapp/src/containers/App.js index d227f83187..fcc99e3860 100644 --- a/scm-ui/ui-webapp/src/containers/App.js +++ b/scm-ui/ui-webapp/src/containers/App.js @@ -1,13 +1,25 @@ // @flow -import React, {Component} from "react"; +import React, { Component } from "react"; import Main from "./Main"; -import {connect} from "react-redux"; -import {translate} from "react-i18next"; -import {withRouter} from "react-router-dom"; -import {fetchMe, getFetchMeFailure, getMe, isAuthenticated, isFetchMePending} from "../modules/auth"; +import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import { withRouter } from "react-router-dom"; +import { + fetchMe, + getFetchMeFailure, + getMe, + isAuthenticated, + isFetchMePending +} from "../modules/auth"; -import {ErrorPage, Footer, Header, Loading, PrimaryNavigation} from "@scm-manager/ui-components"; -import type {Links, Me} from "@scm-manager/ui-types"; +import { + ErrorPage, + Footer, + Header, + Loading, + PrimaryNavigation +} from "@scm-manager/ui-components"; +import type { Links, Me } from "@scm-manager/ui-types"; import { getFetchIndexResourcesFailure, getLinks, diff --git a/scm-ui/ui-webapp/src/containers/Login.js b/scm-ui/ui-webapp/src/containers/Login.js index 28a6729818..fd25c1f0a5 100644 --- a/scm-ui/ui-webapp/src/containers/Login.js +++ b/scm-ui/ui-webapp/src/containers/Login.js @@ -1,12 +1,17 @@ //@flow import React from "react"; -import {connect} from "react-redux"; -import {Redirect, withRouter} from "react-router-dom"; -import {compose} from "redux"; -import {translate} from "react-i18next"; +import { connect } from "react-redux"; +import { Redirect, withRouter } from "react-router-dom"; +import { compose } from "redux"; +import { translate } from "react-i18next"; import styled from "styled-components"; -import {getLoginFailure, isAuthenticated, isLoginPending, login} from "../modules/auth"; -import {getLoginInfoLink, getLoginLink} from "../modules/indexResource"; +import { + getLoginFailure, + isAuthenticated, + isLoginPending, + login +} from "../modules/auth"; +import { getLoginInfoLink, getLoginLink } from "../modules/indexResource"; import LoginInfo from "../components/LoginInfo"; type Props = { diff --git a/scm-ui/ui-webapp/src/modules/auth.js b/scm-ui/ui-webapp/src/modules/auth.js index 02b7b2396a..9145017d0b 100644 --- a/scm-ui/ui-webapp/src/modules/auth.js +++ b/scm-ui/ui-webapp/src/modules/auth.js @@ -1,10 +1,10 @@ // @flow -import type {Me} from "@scm-manager/ui-types"; +import type { Me } from "@scm-manager/ui-types"; import * as types from "./types"; -import {apiClient, UnauthorizedError} from "@scm-manager/ui-components"; -import {isPending} from "./pending"; -import {getFailure} from "./failure"; +import { apiClient, UnauthorizedError } from "@scm-manager/ui-components"; +import { isPending } from "./pending"; +import { getFailure } from "./failure"; import { callFetchIndexResources, fetchIndexResources, diff --git a/scm-ui/ui-webapp/src/modules/auth.test.js b/scm-ui/ui-webapp/src/modules/auth.test.js index 4ee2a9476c..b5d4583e43 100644 --- a/scm-ui/ui-webapp/src/modules/auth.test.js +++ b/scm-ui/ui-webapp/src/modules/auth.test.js @@ -35,7 +35,10 @@ import reducer, { import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; -import {FETCH_INDEXRESOURCES_PENDING, FETCH_INDEXRESOURCES_SUCCESS} from "./indexResource"; +import { + FETCH_INDEXRESOURCES_PENDING, + FETCH_INDEXRESOURCES_SUCCESS +} from "./indexResource"; const me = { name: "tricia", From 4634a8fc45bb5c05b966262a181007c2522b618e Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 17 Oct 2019 10:09:17 +0000 Subject: [PATCH 27/27] Close branch feature/anonymous_access