diff --git a/CHANGELOG.md b/CHANGELOG.md
index d825e633de..0beb76d3e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,7 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Added
- Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278))
-- add link to source file in diff sections ([#1267](https://github.com/scm-manager/scm-manager/pull/1267))
+- Anonymous mode for the web ui ([#1284](https://github.com/scm-manager/scm-manager/pull/1284))
+- Add link to source file in diff sections ([#1267](https://github.com/scm-manager/scm-manager/pull/1267))
- Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283))
- Sign PR merges and commits performed through ui with generated private key ([#1285](https://github.com/scm-manager/scm-manager/pull/1285))
- Add generic popover component to ui-components ([#1285](https://github.com/scm-manager/scm-manager/pull/1285))
@@ -22,7 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New api to resolve SCM-Manager root url ([#1276](https://github.com/scm-manager/scm-manager/pull/1276))
### Changed
-- Help tooltips are now mutliline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
+- Help tooltips are now multiline by default ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
### Fixed
- Fixed unnecessary horizontal scrollbar in modal dialogs ([#1271](https://github.com/scm-manager/scm-manager/pull/1271))
diff --git a/docs/de/user/admin/assets/administration-settings-general.png b/docs/de/user/admin/assets/administration-settings-general.png
index 3be3e6b715..60f981c8c6 100644
Binary files a/docs/de/user/admin/assets/administration-settings-general.png and b/docs/de/user/admin/assets/administration-settings-general.png differ
diff --git a/docs/de/user/admin/settings.md b/docs/de/user/admin/settings.md
index d26d8af3a2..82643a35bb 100644
--- a/docs/de/user/admin/settings.md
+++ b/docs/de/user/admin/settings.md
@@ -26,8 +26,9 @@ Um Angriffe auf den SCM-Manager mit Cross Site Scripting (XSS / XSRF) zu erschwe
#### Plugin-Center-URL
Der SCM-Manager kann ein Plugin-Center anbinden, um schnell und bequem Plugins verwalten zu können. Um ein anderes SCM-Plugin-Center als das vorkonfigurierte zu verwenden, reicht es aus diese URL zu ändern. Läuft der SCM-Manager im Cloudogu EcoSystem kann die Plugin Center URL über einen Eintrag im etcd gesetzt werden.
-#### Anonyme Zugriff erlauben
-Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten (gilt nicht für die Web-Oberflächen) wird dieser anonyme Benutzer verwendet.
+#### Anonyme Zugriff
+Der SCM-Manager 2 hat das Konzept für anonyme Zugriffe über einen "_anonymous"-Benutzer realisiert. Beim Aktivieren des anonymen Zugriffs wird ein neuer Benutzer erstellt mit dem Namen "_anonymous". Dieser Nutzer kann wie ein gewöhnlicher Benutzer für unterschiedliche Aktionen berechtigt werden. Bei einem Zugriff auf den SCM-Manager ohne Zugangsdaten wird dieser anonyme Benutzer verwendet.
+Ist der anonyme Zugriff nur für Protokoll aktiviert, können die REST API und die VCS Protokolle anonym genutzt werden. Wurde der anonyme Zugriff vollständig aktiviert, ist auch ein Zugriff über den Webclient anonym möglich.
Beispiel: Falls der anonyme Zugriff aktiviert ist und der "_anonymous"-Benutzer volle Zugriffsrechte auf ein bestimmtes Git-Repository hat, kann jeder über eine Kommandozeile mit den klassischen Git-Befehlen ohne Zugangsdaten auf dieses Repository zugreifen. Zugriffe über SSH werden aktuell nicht unterstützt.
diff --git a/docs/en/user/admin/assets/administration-settings-general.png b/docs/en/user/admin/assets/administration-settings-general.png
index c29652e101..34be1e5664 100644
Binary files a/docs/en/user/admin/assets/administration-settings-general.png and b/docs/en/user/admin/assets/administration-settings-general.png differ
diff --git a/docs/en/user/admin/settings.md b/docs/en/user/admin/settings.md
index 6f45422ca3..96a606bad0 100644
--- a/docs/en/user/admin/settings.md
+++ b/docs/en/user/admin/settings.md
@@ -26,8 +26,9 @@ Activate this option to make attacks using cross site scripting (XSS / XSRF) on
#### Plugin Center URL
A plugin center can be used to conveniently manage plugins. If you want to use a plugin center that is not the default one, you only have to change this URL. If SCM-Manager is operated as part of a Cloudogu EcoSystem, the plugin center URL can be changed in the etcd.
-#### Enable Anonymous Access
-In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials (this does not apply to access via web UI).
+#### Anonymous Access
+In SCM-Manager 2 the access for anonymous access is realized by using an "_anonymous" user. When the feature is activated, a new user with the name "_anonymous" is created. This user can be authorized just like any other user. This user is used for access to SCM-Manager without login credentials.
+If the anonymous mode is protocol only you may access the SCM-Manager via the REST API and VCS protocols. With fully enabled anonymous access you can also use the webclient without credentials.
Example: If anonymous access is enabled and the "_anonymous" user has full access on a certain Git repository, everybody can access this repository via command line and the classic Git commands without any login credentials. Access via SSH is not supported at this time.
diff --git a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java
index 50202a3410..3a69917df1 100644
--- a/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java
+++ b/scm-core/src/main/java/sonia/scm/config/ScmConfiguration.java
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.config;
@@ -30,6 +30,7 @@ import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus;
+import sonia.scm.security.AnonymousMode;
import sonia.scm.util.HttpUtil;
import sonia.scm.xml.XmlSetStringAdapter;
@@ -161,7 +162,7 @@ public class ScmConfiguration implements Configuration {
* @see http://momentjs.com/docs/#/parsing/
*/
private String dateFormat = DEFAULT_DATEFORMAT;
- private boolean anonymousAccessEnabled = false;
+ private AnonymousMode anonymousMode = AnonymousMode.OFF;
/**
* Enables xsrf cookie protection.
@@ -200,7 +201,7 @@ public class ScmConfiguration implements Configuration {
this.realmDescription = other.realmDescription;
this.dateFormat = other.dateFormat;
this.pluginUrl = other.pluginUrl;
- this.anonymousAccessEnabled = other.anonymousAccessEnabled;
+ this.anonymousMode = other.anonymousMode;
this.enableProxy = other.enableProxy;
this.proxyPort = other.proxyPort;
this.proxyServer = other.proxyServer;
@@ -311,8 +312,24 @@ public class ScmConfiguration implements Configuration {
return realmDescription;
}
+ /**
+ * Returns the currently enabled type of anonymous mode.
+ *
+ * @return anonymous mode
+ * @since 2.4.0
+ */
+ public AnonymousMode getAnonymousMode() {
+ return anonymousMode;
+ }
+
+ /**
+ * Returns {@code true} if anonymous mode is enabled.
+ * @return {@code true} if anonymous mode is enabled
+ * @deprecated since 2.4.0 use {@link ScmConfiguration#getAnonymousMode} instead
+ */
+ @Deprecated
public boolean isAnonymousAccessEnabled() {
- return anonymousAccessEnabled;
+ return anonymousMode != AnonymousMode.OFF;
}
public boolean isDisableGroupingGrid() {
@@ -360,8 +377,28 @@ public class ScmConfiguration implements Configuration {
return skipFailedAuthenticators;
}
+ /**
+ * Enables the anonymous access at protocol level.
+ * @param anonymousAccessEnabled enable or disables the anonymous access
+ * @deprecated since 2.4.0 use {@link ScmConfiguration#setAnonymousMode(AnonymousMode)} instead
+ */
+ @Deprecated
public void setAnonymousAccessEnabled(boolean anonymousAccessEnabled) {
- this.anonymousAccessEnabled = anonymousAccessEnabled;
+ if (anonymousAccessEnabled) {
+ this.anonymousMode = AnonymousMode.PROTOCOL_ONLY;
+ } else {
+ this.anonymousMode = AnonymousMode.OFF;
+ }
+ }
+
+ /**
+ * Configures the anonymous mode.
+ * @param mode type of anonymous mode
+ *
+ * @since 2.4.0
+ */
+ public void setAnonymousMode(AnonymousMode mode) {
+ this.anonymousMode = mode;
}
public void setBaseUrl(String baseUrl) {
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 7079c8fc15..48912b5179 100644
--- a/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java
+++ b/scm-core/src/main/java/sonia/scm/config/ScmConfigurationChangedListener.java
@@ -29,6 +29,7 @@ import com.google.inject.Inject;
import sonia.scm.EagerSingleton;
import sonia.scm.SCMContext;
import sonia.scm.plugin.Extension;
+import sonia.scm.security.AnonymousMode;
import sonia.scm.user.UserManager;
@Extension
@@ -48,7 +49,7 @@ public class ScmConfigurationChangedListener {
}
private void createAnonymousUserIfRequired(ScmConfigurationChangedEvent event) {
- if (event.getConfiguration().isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS)) {
+ if (event.getConfiguration().getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS)) {
userManager.create(SCMContext.ANONYMOUS);
}
}
diff --git a/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java b/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java
new file mode 100644
index 0000000000..0ca7683d32
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/AnonymousMode.java
@@ -0,0 +1,33 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+/**
+ * Available modes for anonymous access
+ * @since 2.4.0
+ */
+public enum AnonymousMode {
+ FULL, PROTOCOL_ONLY, OFF
+}
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 e2ddd1aae2..d99643ee60 100644
--- a/scm-core/src/main/java/sonia/scm/security/Authentications.java
+++ b/scm-core/src/main/java/sonia/scm/security/Authentications.java
@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
public class Authentications {
+ private Authentications() {}
+
public static boolean isAuthenticatedSubjectAnonymous() {
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
}
diff --git a/scm-core/src/main/java/sonia/scm/security/TokenExpiredException.java b/scm-core/src/main/java/sonia/scm/security/TokenExpiredException.java
new file mode 100644
index 0000000000..5f570277a0
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/security/TokenExpiredException.java
@@ -0,0 +1,55 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package sonia.scm.security;
+
+import org.apache.shiro.authc.AuthenticationException;
+
+/**
+ * This exception is thrown if the session token is expired
+ * @since 2.4.0
+ */
+@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here
+public class TokenExpiredException extends AuthenticationException {
+
+ /**
+ * Constructs a new SessionExpiredException.
+ *
+ * @param message the reason for the exception
+ */
+ public TokenExpiredException(String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs a new SessionExpiredException.
+ *
+ * @param message the reason for the exception
+ * @param cause the underlying Throwable that caused this exception to be thrown.
+ */
+ public TokenExpiredException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
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 5a9b30ca4e..04ffe12e21 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
@@ -21,7 +21,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
+
package sonia.scm.web.filter;
//~--- non-JDK imports --------------------------------------------------------
@@ -36,7 +36,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
+import sonia.scm.security.AnonymousMode;
import sonia.scm.security.AnonymousToken;
+import sonia.scm.security.TokenExpiredException;
import sonia.scm.util.HttpUtil;
import sonia.scm.util.Util;
import sonia.scm.web.WebTokenGenerator;
@@ -48,8 +50,6 @@ 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
* an {@link AuthenticationToken}.
@@ -58,25 +58,22 @@ import java.util.Set;
* @since 2.0.0
*/
@Singleton
-public class AuthenticationFilter extends HttpFilter
-{
+public class AuthenticationFilter extends HttpFilter {
- /** marker for failed authentication */
+ private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);
+
+ /**
+ * marker for failed authentication
+ */
private static final String ATTRIBUTE_FAILED_AUTH = "sonia.scm.auth.failed";
- /** Field description */
- private static final String HEADER_AUTHORIZATION = "Authorization";
-
- /** the logger for AuthenticationFilter */
- private static final Logger logger =
- LoggerFactory.getLogger(AuthenticationFilter.class);
-
- //~--- constructors ---------------------------------------------------------
+ private final Set tokenGenerators;
+ protected ScmConfiguration configuration;
/**
* Constructs a new basic authenticaton filter.
*
- * @param configuration scm-manager global configuration
+ * @param configuration scm-manager global configuration
* @param tokenGenerators web token generators
*/
@Inject
@@ -85,47 +82,35 @@ public class AuthenticationFilter extends HttpFilter
this.tokenGenerators = tokenGenerators;
}
- //~--- methods --------------------------------------------------------------
-
/**
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
* an {@link AuthenticationToken}.
*
- * @param request servlet request
+ * @param request servlet request
* @param response servlet response
- * @param chain filter chain
- *
+ * @param chain filter chain
* @throws IOException
* @throws ServletException
*/
@Override
- protected void doFilter(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain)
- throws IOException, ServletException
- {
+ protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
Subject subject = SecurityUtils.getSubject();
AuthenticationToken token = createToken(request);
- if (token != null)
- {
+ if (token != null) {
logger.trace(
"found authentication token on request, start authentication");
handleAuthentication(request, response, chain, subject, token);
- }
- else if (subject.isAuthenticated())
- {
+ } else if (subject.isAuthenticated()) {
logger.trace("user is already authenticated");
processChain(request, response, chain, subject);
- }
- else if (isAnonymousAccessEnabled() && !HttpUtil.isWUIRequest(request))
- {
+ } else if (isAnonymousAccessEnabled()) {
logger.trace("anonymous access granted");
subject.login(new AnonymousToken());
processChain(request, response, chain, subject);
- }
- else
- {
+ } else {
logger.trace("could not find user send unauthorized");
handleUnauthorized(request, response, chain);
}
@@ -135,28 +120,22 @@ public class AuthenticationFilter extends HttpFilter
* Sends status code 403 back to client, if the authentication has failed.
* In all other cases the method will send status code 403 back to client.
*
- * @param request servlet request
+ * @param request servlet request
* @param response servlet response
- * @param chain filter chain
- *
+ * @param chain filter chain
* @throws IOException
* @throws ServletException
- *
* @since 1.8
*/
protected void handleUnauthorized(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain)
- throws IOException, ServletException
- {
+ HttpServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
// send only forbidden, if the authentication has failed.
// see https://bitbucket.org/sdorra/scm-manager/issue/545/git-clone-with-username-in-url-does-not
- if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH)))
- {
+ if (Boolean.TRUE.equals(request.getAttribute(ATTRIBUTE_FAILED_AUTH))) {
sendFailedAuthenticationError(request, response);
- }
- else
- {
+ } else {
sendUnauthorizedError(request, response);
}
}
@@ -164,16 +143,13 @@ public class AuthenticationFilter extends HttpFilter
/**
* Sends an error for a failed authentication back to client.
*
- *
- * @param request http request
+ * @param request http request
* @param response http response
- *
* @throws IOException
*/
protected void sendFailedAuthenticationError(HttpServletRequest request,
- HttpServletResponse response)
- throws IOException
- {
+ HttpServletResponse response)
+ throws IOException {
HttpUtil.sendUnauthorized(request, response,
configuration.getRealmDescription());
}
@@ -181,38 +157,27 @@ public class AuthenticationFilter extends HttpFilter
/**
* Sends an unauthorized error back to client.
*
- *
- * @param request http request
+ * @param request http request
* @param response http response
- *
* @throws IOException
*/
- protected void sendUnauthorizedError(HttpServletRequest request,
- HttpServletResponse response)
- throws IOException
- {
- HttpUtil.sendUnauthorized(request, response,
- configuration.getRealmDescription());
+ protected void sendUnauthorizedError(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ HttpUtil.sendUnauthorized(request, response, configuration.getRealmDescription());
}
/**
* Iterates all {@link WebTokenGenerator} and creates an
* {@link AuthenticationToken} from the given request.
*
- *
* @param request http servlet request
- *
* @return authentication token of {@code null}
*/
- private AuthenticationToken createToken(HttpServletRequest request)
- {
+ private AuthenticationToken createToken(HttpServletRequest request) {
AuthenticationToken token = null;
- for (WebTokenGenerator generator : tokenGenerators)
- {
+ for (WebTokenGenerator generator : tokenGenerators) {
token = generator.createToken(request);
- if (token != null)
- {
+ if (token != null) {
logger.trace("generated web token {} from generator {}",
token.getClass(), generator.getClass());
@@ -226,30 +191,31 @@ public class AuthenticationFilter extends HttpFilter
/**
* Handle authentication with the given {@link AuthenticationToken}.
*
- *
- * @param request http servlet request
+ * @param request http servlet request
* @param response http servlet response
- * @param chain filter chain
- * @param subject subject
- * @param token authentication token
- *
+ * @param chain filter chain
+ * @param subject subject
+ * @param token authentication token
* @throws IOException
* @throws ServletException
*/
private void handleAuthentication(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain, Subject subject,
- AuthenticationToken token)
- throws IOException, ServletException
- {
+ HttpServletResponse response, FilterChain chain, Subject subject,
+ AuthenticationToken token)
+ throws IOException, ServletException {
logger.trace("found basic authorization header, start authentication");
- try
- {
+ try {
subject.login(token);
processChain(request, response, chain, subject);
- }
- catch (AuthenticationException ex)
- {
+ } catch (TokenExpiredException ex) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("{} expired", token.getClass(), ex);
+ } else {
+ logger.debug("{} expired", token.getClass());
+ }
+ handleUnauthorized(request, response, chain);
+ } catch (AuthenticationException ex) {
logger.warn("authentication failed", ex);
handleUnauthorized(request, response, chain);
}
@@ -258,33 +224,26 @@ public class AuthenticationFilter extends HttpFilter
/**
* Process the filter chain.
*
- *
- * @param request http servlet request
+ * @param request http servlet request
* @param response http servlet response
- * @param chain filter chain
- * @param subject subject
- *
+ * @param chain filter chain
+ * @param subject subject
* @throws IOException
* @throws ServletException
*/
private void processChain(HttpServletRequest request,
- HttpServletResponse response, FilterChain chain, Subject subject)
- throws IOException, ServletException
- {
+ HttpServletResponse response, FilterChain chain, Subject subject)
+ throws IOException, ServletException {
String username = Util.EMPTY_STRING;
- if (!subject.isAuthenticated())
- {
+ if (!subject.isAuthenticated()) {
// anonymous access
username = SCMContext.USER_ANONYMOUS;
- }
- else
- {
+ } else {
Object obj = subject.getPrincipal();
- if (obj != null)
- {
+ if (obj != null) {
username = obj.toString();
}
}
@@ -293,24 +252,12 @@ public class AuthenticationFilter extends HttpFilter
response);
}
- //~--- get methods ----------------------------------------------------------
-
/**
* Returns {@code true} if anonymous access is enabled.
*
- *
* @return {@code true} if anonymous access is enabled
*/
- private boolean isAnonymousAccessEnabled()
- {
- return (configuration != null) && configuration.isAnonymousAccessEnabled();
+ private boolean isAnonymousAccessEnabled() {
+ return (configuration != null) && configuration.getAnonymousMode() != AnonymousMode.OFF;
}
-
- //~--- fields ---------------------------------------------------------------
-
- /** set of web token generators */
- private final Set tokenGenerators;
-
- /** scm main configuration */
- protected ScmConfiguration configuration;
}
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 e95ebc2187..cc6bc4272f 100644
--- a/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java
+++ b/scm-core/src/test/java/sonia/scm/config/ScmConfigurationChangedListenerTest.java
@@ -29,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import sonia.scm.security.AnonymousMode;
import sonia.scm.user.UserManager;
import static org.mockito.ArgumentMatchers.any;
@@ -52,7 +53,7 @@ class ScmConfigurationChangedListenerTest {
when(userManager.contains(any())).thenReturn(false);
ScmConfiguration changes = new ScmConfiguration();
- changes.setAnonymousAccessEnabled(true);
+ changes.setAnonymousMode(AnonymousMode.FULL);
scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
@@ -64,7 +65,7 @@ class ScmConfigurationChangedListenerTest {
when(userManager.contains(any())).thenReturn(true);
ScmConfiguration changes = new ScmConfiguration();
- changes.setAnonymousAccessEnabled(true);
+ changes.setAnonymousMode(AnonymousMode.FULL);
scmConfiguration.load(changes);
listener.handleEvent(new ScmConfigurationChangedEvent(scmConfiguration));
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 a9349c83c9..6adf973b43 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
@@ -21,10 +21,8 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
-
-package sonia.scm.web.filter;
-//~--- non-JDK imports --------------------------------------------------------
+package sonia.scm.web.filter;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
@@ -38,6 +36,7 @@ import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.config.ScmConfiguration;
+import sonia.scm.security.BearerToken;
import sonia.scm.web.WebTokenGenerator;
import javax.servlet.FilterChain;
@@ -47,191 +46,98 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
-//~--- JDK imports ------------------------------------------------------------
-
-/**
- *
- * @author Sebastian Sdorra
- */
@RunWith(MockitoJUnitRunner.class)
@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini")
-public class AuthenticationFilterTest
-{
+public class AuthenticationFilterTest {
+
+ @Rule
+ public ShiroRule shiro = new ShiroRule();
+
+ @Mock
+ private FilterChain chain;
+ @Mock
+ private HttpServletRequest request;
+ @Mock
+ private HttpServletResponse response;
+
+ private ScmConfiguration configuration;
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws ServletException
- */
@Test
@SubjectAware(username = "trillian", password = "secret")
- public void testDoFilterAuthenticated() throws IOException, ServletException
- {
+ public void testDoFilterAuthenticated() throws IOException, ServletException {
AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain);
- verify(chain).doFilter(any(HttpServletRequest.class),
- any(HttpServletResponse.class));
+ verify(chain).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class));
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws ServletException
- */
@Test
- public void testDoFilterUnauthorized() throws IOException, ServletException
- {
+ public void testDoFilterUnauthorized() throws IOException, ServletException {
AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain);
- verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED,
- "Authorization Required");
+ verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws ServletException
- */
@Test
- public void testDoFilterWithAuthenticationFailed()
- throws IOException, ServletException
- {
- AuthenticationFilter filter =
- createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
+ public void testDoFilterWithAuthenticationFailed() throws IOException, ServletException {
+ AuthenticationFilter filter = createAuthenticationFilter(new DemoWebTokenGenerator("trillian", "sec"));
filter.doFilter(request, response, chain);
- verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED,
- "Authorization Required");
+
+ verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
}
- /**
- * Method description
- *
- *
- * @throws IOException
- * @throws ServletException
- */
@Test
- public void testDoFilterWithAuthenticationSuccess()
- throws IOException, ServletException
- {
- AuthenticationFilter filter =
- createAuthenticationFilter(new DemoWebTokenGenerator("trillian",
- "secret"));
+ public void testDoFilterWithAuthenticationSuccess() throws IOException, ServletException {
+ AuthenticationFilter filter = createAuthenticationFilter();
filter.doFilter(request, response, chain);
- verify(chain).doFilter(any(HttpServletRequest.class),
- any(HttpServletResponse.class));
+
+ verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
}
- //~--- set methods ----------------------------------------------------------
+ @Test
+ public void testExpiredBearerToken() throws IOException, ServletException {
+ WebTokenGenerator generator = mock(WebTokenGenerator.class);
+ when(generator.createToken(request)).thenReturn(BearerToken.create(null,
+ "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzY21hZG1pbiIsImp0aSI6IjNqUzZ4TzMwUzEiLCJpYXQiOjE1OTY3ODA5Mjg"
+ + "sImV4cCI6MTU5Njc0NDUyOCwic2NtLW1hbmFnZXIucmVmcmVzaEV4cGlyYXRpb24iOjE1OTY4MjQxMjg2MDIsInNjbS1tYW5h"
+ + "Z2VyLnBhcmVudFRva2VuSWQiOiIzalM2eE8zMFMxIn0.utZLmzGZr-M6MP19yrd0dgLPkJ0u1xojwHKQi36_QAs"));
+ AuthenticationFilter filter = createAuthenticationFilter(generator);
+
+ filter.doFilter(request, response, chain);
+ verify(response).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authorization Required");
+ }
- /**
- * Method description
- *
- */
@Before
- public void setUp()
- {
+ public void setUp() {
configuration = new ScmConfiguration();
}
- //~--- methods --------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param generators
- *
- * @return
- */
- private AuthenticationFilter createAuthenticationFilter(
- WebTokenGenerator... generators)
- {
- return new AuthenticationFilter(configuration,
- ImmutableSet.copyOf(generators));
+ private AuthenticationFilter createAuthenticationFilter(WebTokenGenerator... generators) {
+ return new AuthenticationFilter(configuration, ImmutableSet.copyOf(generators));
}
- //~--- inner classes --------------------------------------------------------
+ private static class DemoWebTokenGenerator implements WebTokenGenerator {
- /**
- * Class description
- *
- *
- * @version Enter version here..., 15/02/21
- * @author Enter your name here...
- */
- private static class DemoWebTokenGenerator implements WebTokenGenerator
- {
+ private final String username;
+ private final String password;
- /**
- * Constructs ...
- *
- *
- * @param username
- * @param password
- */
- public DemoWebTokenGenerator(String username, String password)
- {
+ public DemoWebTokenGenerator(String username, String password) {
this.username = username;
this.password = password;
}
- //~--- methods ------------------------------------------------------------
-
- /**
- * Method description
- *
- *
- * @param request
- *
- * @return
- */
@Override
- public AuthenticationToken createToken(HttpServletRequest request)
- {
+ public AuthenticationToken createToken(HttpServletRequest request) {
return new UsernamePasswordToken(username, password);
}
-
- //~--- fields -------------------------------------------------------------
-
- /** Field description */
- private final String password;
-
- /** Field description */
- private final String username;
}
- //~--- fields ---------------------------------------------------------------
-
- /** Field description */
- @Rule
- public ShiroRule shiro = new ShiroRule();
-
- /** Field description */
- @Mock
- private FilterChain chain;
-
- /** Field description */
- private ScmConfiguration configuration;
-
- /** Field description */
- @Mock
- private HttpServletRequest request;
-
- /** Field description */
- @Mock
- private HttpServletResponse response;
}
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 0994ca0dcb..603942e4f7 100644
--- a/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java
+++ b/scm-it/src/test/java/sonia/scm/it/AnonymousAccessITCase.java
@@ -41,6 +41,7 @@ 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 sonia.scm.security.AnonymousMode;
import javax.json.Json;
import javax.json.JsonArray;
@@ -77,10 +78,10 @@ class AnonymousAccessITCase {
@Nested
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
- class WithAnonymousAccess {
+ class WithProtocolOnlyAnonymousAccess {
@BeforeAll
void enableAnonymousAccess() {
- setAnonymousAccess(true);
+ setAnonymousAccess(AnonymousMode.PROTOCOL_ONLY);
}
@BeforeEach
@@ -120,7 +121,7 @@ class AnonymousAccessITCase {
@BeforeEach
void grantAnonymousAccessToRepo() {
- ScmTypes.availableScmTypes().stream().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type));
+ ScmTypes.availableScmTypes().forEach(type -> TestData.createUserPermission(USER_ANONYMOUS, WRITE, type));
}
@ParameterizedTest
@@ -142,13 +143,84 @@ class AnonymousAccessITCase {
@AfterAll
void disableAnonymousAccess() {
- setAnonymousAccess(false);
+ setAnonymousAccess(AnonymousMode.OFF);
}
}
- private static void setAnonymousAccess(boolean anonymousAccessEnabled) {
+ @Nested
+ @TestInstance(TestInstance.Lifecycle.PER_CLASS)
+ class WithFullAnonymousAccess {
+ @BeforeAll
+ void enableAnonymousAccess() {
+ setAnonymousAccess(AnonymousMode.FULL);
+ }
+
+ @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, @TempDir Path temporaryFolder) {
+ assertThrows(RepositoryClientException.class, () -> RepositoryUtil.createAnonymousRepositoryClient(type, Files.createDirectories(temporaryFolder).toFile()));
+ }
+ }
+
+ @Nested
+ class WithAnonymousAccessForRepository {
+
+ @BeforeEach
+ void grantAnonymousAccessToRepo() {
+ ScmTypes.availableScmTypes().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, @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(AnonymousMode.OFF);
+ }
+ }
+
+ private static void setAnonymousAccess(AnonymousMode anonymousMode) {
RestUtil.given("application/vnd.scmm-config+json;v=2")
- .body(createConfig(anonymousAccessEnabled))
+ .body(createConfig(anonymousMode))
.when()
.put(RestUtil.REST_BASE_URL.toASCIIString() + "config")
@@ -157,12 +229,12 @@ class AnonymousAccessITCase {
.statusCode(HttpServletResponse.SC_NO_CONTENT);
}
- private static String createConfig(boolean anonymousAccessEnabled) {
+ private static String createConfig(AnonymousMode anonymousMode) {
JsonArray emptyArray = Json.createBuilderFactory(emptyMap()).createArrayBuilder().build();
return JSON_BUILDER
.add("adminGroups", emptyArray)
.add("adminUsers", emptyArray)
- .add("anonymousAccessEnabled", anonymousAccessEnabled)
+ .add("anonymousMode", anonymousMode.toString())
.add("baseUrl", "https://next-scm.cloudogu.com/scm")
.add("dateFormat", "YYYY-MM-DD HH:mm:ss")
.add("disableGroupingGrid", false)
diff --git a/scm-ui/e2e-tests/cypress.json b/scm-ui/e2e-tests/cypress.json
new file mode 100644
index 0000000000..03e8546581
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress.json
@@ -0,0 +1,3 @@
+{
+ "baseUrl": "http://localhost:8081/scm"
+}
diff --git a/scm-ui/e2e-tests/cypress/integration/anonymousMode_disabled.spec.js b/scm-ui/e2e-tests/cypress/integration/anonymousMode_disabled.spec.js
new file mode 100644
index 0000000000..0380681777
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/integration/anonymousMode_disabled.spec.js
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+describe("With Anonymous mode disabled", () => {
+ before("Disable anonymous access", () => {
+ cy.login("scmadmin", "scmadmin");
+ cy.setAnonymousMode("OFF");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+
+ it("Should show login page without primary navigation", () => {
+ cy.byTestId("login-button");
+ cy.containsNotByTestId("div", "primary-navigation-login");
+ cy.containsNotByTestId("div", "primary-navigation-repositories");
+ });
+ it("Should redirect after login", () => {
+ cy.login("scmadmin", "scmadmin");
+
+ cy.visit("/me");
+ cy.byTestId("footer-user-profile");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+});
diff --git a/scm-ui/e2e-tests/cypress/integration/anonymousMode_full.spec.js b/scm-ui/e2e-tests/cypress/integration/anonymousMode_full.spec.js
new file mode 100644
index 0000000000..d9652cdcf7
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/integration/anonymousMode_full.spec.js
@@ -0,0 +1,86 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+describe("With Anonymous mode fully enabled", () => {
+ before("Set anonymous mode to full", () => {
+ cy.login("scmadmin", "scmadmin");
+ cy.setAnonymousMode("FULL");
+
+ // Give anonymous user permissions
+ cy.byTestId("primary-navigation-users").click();
+ cy.byTestId("_anonymous").click();
+ cy.byTestId("user-settings-link").click();
+ cy.byTestId("user-permissions-link").click();
+ cy.byTestId("read-all-repositories").click();
+ cy.byTestId("set-permissions-button").click();
+
+ cy.byTestId("primary-navigation-logout").click();
+ });
+
+ it("Should show repositories overview with Login button in primary navigation", () => {
+ cy.visit("/repos/");
+ cy.byTestId("repository-overview-filter");
+ cy.byTestId("scm-anonymous");
+ cy.byTestId("primary-navigation-login");
+ });
+ it("Should show login page on url", () => {
+ cy.visit("/login/");
+ cy.byTestId("login-button");
+ });
+ it("Should show login page on link click", () => {
+ cy.visit("/repos/");
+ cy.byTestId("repository-overview-filter");
+ cy.byTestId("primary-navigation-login").click();
+ cy.byTestId("login-button");
+ });
+ it("Should login and direct to repositories overview", () => {
+ cy.login("scmadmin", "scmadmin");
+
+ cy.visit("/login");
+ cy.byTestId("scm-administrator");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+ it("Should logout and direct to login page", () => {
+ cy.login("scmadmin", "scmadmin");
+
+ cy.visit("/repos/");
+ cy.byTestId("repository-overview-filter");
+ cy.byTestId("scm-administrator");
+ cy.byTestId("primary-navigation-logout").click();
+ cy.byTestId("login-button");
+ });
+ it("Anonymous user should not be able to change password", () => {
+ cy.visit("/repos/");
+ cy.byTestId("footer-user-profile").click();
+ cy.byTestId("scm-anonymous");
+ cy.containsNotByTestId("ul", "user-settings-link");
+ cy.get("section").not("Change password");
+ });
+
+ after("Disable anonymous access", () => {
+ cy.login("scmadmin", "scmadmin");
+ cy.setAnonymousMode("OFF");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+});
diff --git a/scm-ui/e2e-tests/cypress/integration/anonymousMode_protocolOnly.spec.js b/scm-ui/e2e-tests/cypress/integration/anonymousMode_protocolOnly.spec.js
new file mode 100644
index 0000000000..fd223f3ae4
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/integration/anonymousMode_protocolOnly.spec.js
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+describe("With Anonymous mode protocol only enabled", () => {
+ before("Set anonymous mode to protocol only", () => {
+ cy.login("scmadmin", "scmadmin");
+ cy.setAnonymousMode("PROTOCOL_ONLY");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+
+ it("Should show login page without primary navigation", () => {
+ cy.visit("/repos/");
+ cy.byTestId("login-button");
+ cy.containsNotByTestId("div", "primary-navigation-login");
+ cy.containsNotByTestId("div", "primary-navigation-repositories");
+ });
+
+ after("Disable anonymous access", () => {
+ cy.login("scmadmin", "scmadmin");
+ cy.setAnonymousMode("OFF");
+ cy.byTestId("primary-navigation-logout").click();
+ });
+});
diff --git a/scm-ui/e2e-tests/cypress/plugins/index.js b/scm-ui/e2e-tests/cypress/plugins/index.js
new file mode 100644
index 0000000000..588beee610
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/plugins/index.js
@@ -0,0 +1,44 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+///
+// ***********************************************************
+// This example plugins/index.js can be used to load plugins
+//
+// You can change the location of this file or turn off loading
+// the plugins file with the 'pluginsFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/plugins-guide
+// ***********************************************************
+
+// This function is called when a project is opened or re-opened (e.g. due to
+// the project's config changing)
+
+/**
+ * @type {Cypress.PluginConfig}
+ */
+module.exports = (on, config) => {
+ // `on` is used to hook into various events Cypress emits
+ // `config` is the resolved Cypress config
+}
diff --git a/scm-ui/e2e-tests/cypress/support/commands.js b/scm-ui/e2e-tests/cypress/support/commands.js
new file mode 100644
index 0000000000..2e7bb6ec65
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/support/commands.js
@@ -0,0 +1,69 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+// ***********************************************
+// This example commands.js shows you how to
+// create various custom commands and overwrite
+// existing commands.
+//
+// For more comprehensive examples of custom
+// commands please read more here:
+// https://on.cypress.io/custom-commands
+// ***********************************************
+//
+//
+// -- This is a parent command --
+// Cypress.Commands.add("login", (email, password) => { ... })
+//
+//
+// -- This is a child command --
+// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
+//
+//
+// -- This is a dual command --
+// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
+//
+//
+// -- This will overwrite an existing command --
+// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
+
+const login = (username, password) => {
+ cy.visit("/login");
+ cy.byTestId("username-input").type(username);
+ cy.byTestId("password-input").type(password);
+ cy.byTestId("login-button").click();
+};
+
+const setAnonymousMode = anonymousMode => {
+ cy.byTestId("primary-navigation-admin").click();
+ cy.byTestId("admin-settings-link").click();
+ cy.byTestId("anonymous-mode-select")
+ .select(anonymousMode)
+ .should("have.value", anonymousMode);
+ cy.byTestId("submit-button").click();
+};
+
+Cypress.Commands.add("login", login);
+Cypress.Commands.add("setAnonymousMode", setAnonymousMode);
+Cypress.Commands.add("byTestId", testId => cy.get(`[data-testid=${testId}]`));
+Cypress.Commands.add("containsNotByTestId", (container, testId) => cy.get(container).not(`[data-testid=${testId}]`));
diff --git a/scm-ui/e2e-tests/cypress/support/index.js b/scm-ui/e2e-tests/cypress/support/index.js
new file mode 100644
index 0000000000..7ef6a0ade0
--- /dev/null
+++ b/scm-ui/e2e-tests/cypress/support/index.js
@@ -0,0 +1,43 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2020-present Cloudogu GmbH and Contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+// ***********************************************************
+// This example support/index.js is processed and
+// loaded automatically before your test files.
+//
+// This is a great place to put global configuration and
+// behavior that modifies Cypress.
+//
+// You can change the location of this file or turn off
+// automatically serving support files with the
+// 'supportFile' configuration option.
+//
+// You can read more here:
+// https://on.cypress.io/configuration
+// ***********************************************************
+
+// Import commands.js using ES2015 syntax:
+import './commands'
+
+// Alternatively you can use CommonJS syntax:
+// require('./commands')
diff --git a/scm-ui/e2e-tests/package.json b/scm-ui/e2e-tests/package.json
new file mode 100644
index 0000000000..fd7a7c2c23
--- /dev/null
+++ b/scm-ui/e2e-tests/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@scm-manager/e2e-tests",
+ "version": "2.4.0-SNAPSHOT",
+ "description": "End to end Tests for SCM-Manager",
+ "main": "index.js",
+ "author": "Eduard Heimbuch ",
+ "license": "MIT",
+ "private": false,
+ "devDependencies": {
+ "cypress": "^4.12.0",
+ "eslint-plugin-cypress": "^2.11.1"
+ },
+ "prettier": "@scm-manager/prettier-config",
+ "eslintConfig": {
+ "extends": "@scm-manager/eslint-config",
+ "plugins": [
+ "cypress"
+ ],
+ "env": {
+ "cypress/globals": true
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/scm-ui/ui-components/src/Icon.tsx b/scm-ui/ui-components/src/Icon.tsx
index 91edfbfd10..8e87871f83 100644
--- a/scm-ui/ui-components/src/Icon.tsx
+++ b/scm-ui/ui-components/src/Icon.tsx
@@ -23,6 +23,7 @@
*/
import React from "react";
import classNames from "classnames";
+import { createAttributesForTesting } from "./devBuild";
type Props = {
title?: string;
@@ -31,6 +32,7 @@ type Props = {
color: string;
className?: string;
onClick?: () => void;
+ testId?: string;
};
export default class Icon extends React.Component {
@@ -40,12 +42,23 @@ export default class Icon extends React.Component {
};
render() {
- const { title, iconStyle, name, color, className, onClick } = this.props;
+ const { title, iconStyle, name, color, className, onClick, testId } = this.props;
if (title) {
return (
-
+
);
}
- return ;
+ return (
+
+ );
}
}
diff --git a/scm-ui/ui-components/src/OverviewPageActions.tsx b/scm-ui/ui-components/src/OverviewPageActions.tsx
index 03df29b869..a5c90960f0 100644
--- a/scm-ui/ui-components/src/OverviewPageActions.tsx
+++ b/scm-ui/ui-components/src/OverviewPageActions.tsx
@@ -32,11 +32,12 @@ type Props = RouteComponentProps & {
showCreateButton: boolean;
link: string;
label?: string;
+ testId?: string;
};
class OverviewPageActions extends React.Component {
render() {
- const { history, location, link } = this.props;
+ const { history, location, link, testId } = this.props;
return (
<>
{
filter={filter => {
history.push(`/${link}/?q=${filter}`);
}}
+ testId={testId + "-filter"}
/>
{this.renderCreateButton()}
>
diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
index 688085b3fe..5754f126fd 100644
--- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
+++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap
@@ -44720,10 +44720,10 @@ exports[`Storyshots Layout|Footer Default 1`] = `