Merge with develop branch

This commit is contained in:
Sebastian Sdorra
2020-08-12 14:30:26 +02:00
99 changed files with 2199 additions and 557 deletions

View File

@@ -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))

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -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.

View File

@@ -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 <a href="http://momentjs.com/docs/#/parsing/" target="_blank">http://momentjs.com/docs/#/parsing/</a>
*/
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) {

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
public class Authentications {
private Authentications() {}
public static boolean isAuthenticatedSubjectAnonymous() {
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
}

View File

@@ -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);
}
}

View File

@@ -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,20 +58,17 @@ 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<WebTokenGenerator> tokenGenerators;
protected ScmConfiguration configuration;
/**
* Constructs a new basic authenticaton filter.
@@ -85,8 +82,6 @@ public class AuthenticationFilter extends HttpFilter
this.tokenGenerators = tokenGenerators;
}
//~--- methods --------------------------------------------------------------
/**
* Handles authentication, if a one of the {@link WebTokenGenerator} returns
* an {@link AuthenticationToken}.
@@ -94,38 +89,28 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request
* @param response servlet response
* @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);
}
@@ -138,25 +123,19 @@ public class AuthenticationFilter extends HttpFilter
* @param request servlet request
* @param response servlet response
* @param chain filter chain
*
* @throws IOException
* @throws ServletException
*
* @since 1.8
*/
protected void handleUnauthorized(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
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 response http response
*
* @throws IOException
*/
protected void sendFailedAuthenticationError(HttpServletRequest request,
HttpServletResponse response)
throws IOException
{
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 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 response http servlet response
* @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
{
throws IOException, ServletException {
logger.trace("found basic authorization header, start authentication");
try
{
try {
subject.login(token);
processChain(request, response, chain, subject);
} catch (TokenExpiredException ex) {
if (logger.isTraceEnabled()) {
logger.trace("{} expired", token.getClass(), ex);
} else {
logger.debug("{} expired", token.getClass());
}
catch (AuthenticationException ex)
{
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 response http servlet response
* @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
{
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<WebTokenGenerator> tokenGenerators;
/** scm main configuration */
protected ScmConfiguration configuration;
}

View File

@@ -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));

View File

@@ -24,8 +24,6 @@
package sonia.scm.web.filter;
//~--- non-JDK imports --------------------------------------------------------
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet;
@@ -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;
}

View File

@@ -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)

View File

@@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:8081/scm"
}

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

@@ -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.
*/
/// <reference types="cypress" />
// ***********************************************************
// 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
}

View File

@@ -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}]`));

View File

@@ -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')

View File

@@ -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 <eduard.heimbuch@cloudogu.com>",
"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"
}
}

View File

@@ -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<Props> {
@@ -40,12 +42,23 @@ export default class Icon extends React.Component<Props> {
};
render() {
const { title, iconStyle, name, color, className, onClick } = this.props;
const { title, iconStyle, name, color, className, onClick, testId } = this.props;
if (title) {
return (
<i onClick={onClick} title={title} className={classNames(iconStyle, "fa-fw", "fa-" + name, `has-text-${color}`, className)} />
<i
onClick={onClick}
title={title}
className={classNames(iconStyle, "fa-fw", "fa-" + name, `has-text-${color}`, className)}
{...createAttributesForTesting(testId)}
/>
);
}
return <i onClick={onClick} className={classNames(iconStyle, "fa-" + name, `has-text-${color}`, className)} />;
return (
<i
onClick={onClick}
className={classNames(iconStyle, "fa-" + name, `has-text-${color}`, className)}
{...createAttributesForTesting(testId)}
/>
);
}
}

View File

@@ -32,11 +32,12 @@ type Props = RouteComponentProps & {
showCreateButton: boolean;
link: string;
label?: string;
testId?: string;
};
class OverviewPageActions extends React.Component<Props> {
render() {
const { history, location, link } = this.props;
const { history, location, link, testId } = this.props;
return (
<>
<FilterInput
@@ -44,6 +45,7 @@ class OverviewPageActions extends React.Component<Props> {
filter={filter => {
history.push(`/${link}/?q=${filter}`);
}}
testId={testId + "-filter"}
/>
{this.renderCreateButton()}
</>

View File

@@ -44720,10 +44720,10 @@ exports[`Storyshots Layout|Footer Default 1`] = `
<li>
<a
className=""
href="/me/settings/publicKeys"
href="/me"
onClick={[Function]}
>
profile.publicKeysNavLink
footer.user.profile
</a>
</li>
</ul>
@@ -44810,6 +44810,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<div
className="FooterSection__Title-lx0ikb-0 gUQuRF"
>
<div>
<span
className="Footer__AvatarContainer-k70cxq-1 faWKKx image is-rounded"
>
@@ -44821,6 +44822,7 @@ exports[`Storyshots Layout|Footer Full 1`] = `
</span>
Trillian McMillian
</div>
</div>
<ul
className="FooterSection__Menu-lx0ikb-1 idmusL"
>
@@ -44845,10 +44847,10 @@ exports[`Storyshots Layout|Footer Full 1`] = `
<li>
<a
className=""
href="/me/settings/publicKeys"
href="/me"
onClick={[Function]}
>
profile.publicKeysNavLink
footer.user.profile
</a>
</li>
<li>
@@ -44971,6 +44973,7 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
<div
className="FooterSection__Title-lx0ikb-0 gUQuRF"
>
<div>
<span
className="Footer__AvatarContainer-k70cxq-1 faWKKx image is-rounded"
>
@@ -44982,6 +44985,7 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
</span>
Trillian McMillian
</div>
</div>
<ul
className="FooterSection__Menu-lx0ikb-1 idmusL"
>
@@ -45006,10 +45010,10 @@ exports[`Storyshots Layout|Footer With Avatar 1`] = `
<li>
<a
className=""
href="/me/settings/publicKeys"
href="/me"
onClick={[Function]}
>
profile.publicKeysNavLink
footer.user.profile
</a>
</li>
</ul>
@@ -45126,10 +45130,10 @@ exports[`Storyshots Layout|Footer With Plugin Links 1`] = `
<li>
<a
className=""
href="/me/settings/publicKeys"
href="/me"
onClick={[Function]}
>
profile.publicKeysNavLink
footer.user.profile
</a>
</li>
<li>

View File

@@ -25,6 +25,7 @@ import React, { MouseEvent, ReactNode } from "react";
import classNames from "classnames";
import { withRouter, RouteComponentProps } from "react-router-dom";
import Icon from "../Icon";
import { createAttributesForTesting } from "../devBuild";
export type ButtonProps = {
label?: string;
@@ -37,6 +38,7 @@ export type ButtonProps = {
fullWidth?: boolean;
reducedMobile?: boolean;
children?: ReactNode;
testId?: string;
};
type Props = ButtonProps &
@@ -73,7 +75,8 @@ class Button extends React.Component<Props> {
icon,
fullWidth,
reducedMobile,
children
children,
testId
} = this.props;
const loadingClass = loading ? "is-loading" : "";
const fullWidthClass = fullWidth ? "is-fullwidth" : "";
@@ -86,6 +89,7 @@ class Button extends React.Component<Props> {
disabled={disabled}
onClick={this.onClick}
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, reducedMobileClass, className)}
{...createAttributesForTesting(testId)}
>
<span className="icon is-medium">
<Icon name={icon} color="inherit" />
@@ -104,6 +108,7 @@ class Button extends React.Component<Props> {
disabled={disabled}
onClick={this.onClick}
className={classNames("button", "is-" + color, loadingClass, fullWidthClass, className)}
{...createAttributesForTesting(testId)}
>
{label} {children}
</button>

View File

@@ -26,6 +26,7 @@ import Button, { ButtonProps } from "./Button";
type SubmitButtonProps = ButtonProps & {
scrollToTop: boolean;
testId?: string;
};
class SubmitButton extends React.Component<SubmitButtonProps> {
@@ -34,7 +35,7 @@ class SubmitButton extends React.Component<SubmitButtonProps> {
};
render() {
const { action, scrollToTop } = this.props;
const { action, scrollToTop, testId } = this.props;
return (
<Button
type="submit"
@@ -48,6 +49,7 @@ class SubmitButton extends React.Component<SubmitButtonProps> {
window.scrollTo(0, 0);
}
}}
testId={testId ? testId : "submit-button"}
/>
);
}

View File

@@ -0,0 +1,94 @@
/*
* 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.
*/
import { createAttributesForTesting, isDevBuild } from "./devBuild";
describe("devbuild tests", () => {
let env: string | undefined;
beforeAll(() => {
env = process.env.NODE_ENV;
});
afterAll(() => {
process.env.NODE_ENV = env;
});
describe("isDevBuild tests", () => {
it("should return true for development", () => {
process.env.NODE_ENV = "development";
expect(isDevBuild()).toBe(true);
});
it("should return false for production", () => {
process.env.NODE_ENV = "production";
expect(isDevBuild()).toBe(false);
});
});
describe("createAttributesForTesting in non development mode", () => {
beforeAll(() => {
process.env.NODE_ENV = "production";
});
it("should return undefined for non development", () => {
const attributes = createAttributesForTesting("123");
expect(attributes).toBeUndefined();
});
});
describe("createAttributesForTesting in development mode", () => {
beforeAll(() => {
process.env.NODE_ENV = "development";
});
it("should return undefined for non development", () => {
const attributes = createAttributesForTesting("123");
expect(attributes).toBeDefined();
});
it("should return undefined for undefined testid", () => {
const attributes = createAttributesForTesting();
expect(attributes).toBeUndefined();
});
it("should remove spaces from test id", () => {
const attributes = createAttributesForTesting("heart of gold");
if (attributes) {
expect(attributes["data-testid"]).toBe("heart-of-gold");
} else {
throw new Error("attributes should be defined");
}
});
it("should lower case test id", () => {
const attributes = createAttributesForTesting("HeartOfGold");
if (attributes) {
expect(attributes["data-testid"]).toBe("heartofgold");
} else {
throw new Error("attributes should be defined");
}
});
});
});

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
export const isDevBuild = () => process.env.NODE_ENV === "development";
export const createAttributesForTesting = (testId?: string) => {
if (!testId || !isDevBuild()) {
return undefined;
}
return {
"data-testid": normalizeTestId(testId)
};
};
const normalizeTestId = (testId: string) => {
let id = testId.toLowerCase();
while (id.includes(" ")) {
id = id.replace(" ", "-");
}
return id;
};

View File

@@ -35,6 +35,7 @@ type Props = {
title?: string;
disabled?: boolean;
helpText?: string;
testId?: string;
};
export default class Checkbox extends React.Component<Props> {
@@ -59,7 +60,7 @@ export default class Checkbox extends React.Component<Props> {
};
render() {
const { label, checked, indeterminate, disabled } = this.props;
const { label, checked, indeterminate, disabled, testId } = this.props;
return (
<div className="field">
{this.renderLabelWithHelp()}
@@ -70,7 +71,7 @@ export default class Checkbox extends React.Component<Props> {
but bulma does.
// @ts-ignore */}
<label className="checkbox" disabled={disabled}>
<TriStateCheckbox checked={checked} indeterminate={indeterminate} disabled={disabled} />
<TriStateCheckbox checked={checked} indeterminate={indeterminate} disabled={disabled} testId={testId} />
{label}
{this.renderHelp()}
</label>

View File

@@ -24,10 +24,12 @@
import React, { ChangeEvent, FormEvent } from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import styled from "styled-components";
import { createAttributesForTesting } from "../devBuild";
type Props = WithTranslation & {
filter: (p: string) => void;
value?: string;
testId?: string;
};
type State = {
@@ -58,9 +60,9 @@ class FilterInput extends React.Component<Props, State> {
};
render() {
const { t } = this.props;
const { t, testId } = this.props;
return (
<form className="input-field" onSubmit={this.handleSubmit}>
<form className="input-field" onSubmit={this.handleSubmit} {...createAttributesForTesting(testId)}>
<div className="control has-icons-left">
<FixedHeightInput
className="input"

View File

@@ -24,6 +24,7 @@
import React, { ChangeEvent, KeyboardEvent } from "react";
import classNames from "classnames";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
import { createAttributesForTesting } from "../devBuild";
type Props = {
label?: string;
@@ -39,6 +40,7 @@ type Props = {
disabled?: boolean;
helpText?: string;
className?: string;
testId?: string;
};
class InputField extends React.Component<Props> {
@@ -80,7 +82,8 @@ class InputField extends React.Component<Props> {
disabled,
label,
helpText,
className
className,
testId
} = this.props;
const errorView = validationError ? "is-danger" : "";
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
@@ -99,6 +102,7 @@ class InputField extends React.Component<Props> {
onChange={this.handleInput}
onKeyPress={this.handleKeyPress}
disabled={disabled}
{...createAttributesForTesting(testId)}
/>
</div>
{helper}

View File

@@ -24,6 +24,7 @@
import React, { ChangeEvent } from "react";
import classNames from "classnames";
import LabelWithHelpIcon from "./LabelWithHelpIcon";
import {createAttributesForTesting} from "../devBuild";
export type SelectItem = {
value: string;
@@ -39,6 +40,7 @@ type Props = {
loading?: boolean;
helpText?: string;
disabled?: boolean;
testId?: string;
};
class Select extends React.Component<Props> {
@@ -57,7 +59,7 @@ class Select extends React.Component<Props> {
};
render() {
const { options, value, label, helpText, loading, disabled } = this.props;
const { options, value, label, helpText, loading, disabled, testId } = this.props;
const loadingClass = loading ? "is-loading" : "";
return (
@@ -71,6 +73,7 @@ class Select extends React.Component<Props> {
value={value}
onChange={this.handleInput}
disabled={disabled}
{...createAttributesForTesting(testId)}
>
{options.map(opt => {
return (

View File

@@ -29,9 +29,10 @@ type Props = {
indeterminate?: boolean;
disabled?: boolean;
label?: string;
testId?: string;
};
const TriStateCheckbox: FC<Props> = ({ checked, indeterminate, disabled, label }) => {
const TriStateCheckbox: FC<Props> = ({ checked, indeterminate, disabled, label, testId }) => {
let icon;
if (indeterminate) {
icon = "minus-square";
@@ -57,8 +58,11 @@ const TriStateCheckbox: FC<Props> = ({ checked, indeterminate, disabled, label }
color = "black";
}
return <><Icon iconStyle={"is-outlined"} name={icon} className={className} color={color} />{" "}
{label}</>;
return (
<>
<Icon iconStyle={"is-outlined"} name={icon} className={className} color={color} testId={testId} /> {label}
</>
);
};
export default TriStateCheckbox;

View File

@@ -85,6 +85,7 @@ export { default as comparators } from "./comparators";
export { apiClient } from "./apiclient";
export * from "./errors";
export { isDevBuild, createAttributesForTesting } from "./devBuild";
export * from "./avatar";
export * from "./buttons";

View File

@@ -22,8 +22,8 @@
* SOFTWARE.
*/
import React, { FC } from "react";
import { Me, Links } from "@scm-manager/ui-types";
import { useBinder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { Links, Me } from "@scm-manager/ui-types";
import { ExtensionPoint, useBinder } from "@scm-manager/ui-extensions";
import { AvatarImage } from "../avatar";
import NavLink from "../navigation/NavLink";
import FooterSection from "./FooterSection";
@@ -31,6 +31,7 @@ import styled from "styled-components";
import { EXTENSION_POINT } from "../avatar/Avatar";
import ExternalNavLink from "../navigation/ExternalNavLink";
import { useTranslation } from "react-i18next";
import { createAttributesForTesting } from "../devBuild";
type Props = {
me?: Me;
@@ -43,11 +44,13 @@ type TitleWithIconsProps = {
icon: string;
};
const TitleWithIcon: FC<TitleWithIconsProps> = ({ icon, title }) => (
const TitleWithIcon: FC<TitleWithIconsProps> = ({ icon, title }) => {
return (
<>
<i className={`fas fa-${icon} fa-fw`} /> {title}
<i className={`fas fa-${icon} fa-fw`} {...createAttributesForTesting(title)} /> {title}
</>
);
);
};
type TitleWithAvatarProps = {
me: Me;
@@ -66,12 +69,12 @@ const AvatarContainer = styled.span`
`;
const TitleWithAvatar: FC<TitleWithAvatarProps> = ({ me }) => (
<>
<div {...createAttributesForTesting(me.displayName)}>
<AvatarContainer className="image is-rounded">
<VCenteredAvatar person={me} representation="rounded" />
</AvatarContainer>
{me.displayName}
</>
</div>
);
const Footer: FC<Props> = ({ me, version, links }) => {
@@ -96,7 +99,9 @@ const Footer: FC<Props> = ({ me, version, links }) => {
<FooterSection title={meSectionTile}>
<NavLink to="/me" label={t("footer.user.profile")} />
<NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />
<NavLink to="/me/settings/publicKeys" label={t("profile.publicKeysNavLink")} />
<NavLink to="/me" label={t("footer.user.profile")} testId="footer-user-profile" />
{me?._links?.password && <NavLink to="/me/settings/password" label={t("profile.changePasswordNavLink")} />}
{me?._links?.publicKeys && <NavLink to="/me/settings/publicKeys" label={t("profile.publicKeysNavLink")} />}
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</FooterSection>
<FooterSection title={<TitleWithIcon title={t("footer.information.title")} icon="info-circle" />}>

View File

@@ -28,14 +28,16 @@ import { RoutingProps } from "./RoutingProps";
import { FC } from "react";
import useMenuContext from "./MenuContext";
import useActiveMatch from "./useActiveMatch";
import {createAttributesForTesting} from "../devBuild";
type Props = RoutingProps & {
label: string;
title?: string;
icon?: string;
testId?: string;
};
const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, icon, label, title }) => {
const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, icon, label, title, testId }) => {
const active = useActiveMatch({ to, activeWhenMatch, activeOnlyWhenExact });
const context = useMenuContext();
@@ -52,7 +54,11 @@ const NavLink: FC<Props> = ({ to, activeWhenMatch, activeOnlyWhenExact, icon, la
return (
<li title={collapsed ? title : undefined}>
<Link className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")} to={to}>
<Link
className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
{...createAttributesForTesting(testId)}
>
{showIcon}
{collapsed ? null : label}
</Link>

View File

@@ -40,7 +40,7 @@ class PrimaryNavigation extends React.Component<Props> {
return (to: string, match: string, label: string, linkName: string) => {
const link = links[linkName];
if (link) {
const navigationItem = <PrimaryNavigationLink to={to} match={match} label={t(label)} key={linkName} />;
const navigationItem = <PrimaryNavigationLink testId={label.replace(".", "-")} to={to} match={match} label={t(label)} key={linkName} />;
navigationItems.push(navigationItem);
}
};
@@ -63,6 +63,23 @@ class PrimaryNavigation extends React.Component<Props> {
}
};
appendLogin = (navigationItems: ReactNode[], append: Appender) => {
const { t, links } = this.props;
const props = {
links,
label: t("primary-navigation.login")
};
if (binder.hasExtension("primary-navigation.login", props)) {
navigationItems.push(
<ExtensionPoint key="primary-navigation.login" name="primary-navigation.login" props={props} />
);
} else {
append("/login", "/login", "primary-navigation.login", "login");
}
};
createNavigationItems = () => {
const navigationItems: ReactNode[] = [];
const { t, links } = this.props;
@@ -95,6 +112,7 @@ class PrimaryNavigation extends React.Component<Props> {
);
this.appendLogout(navigationItems, append);
this.appendLogin(navigationItems, append);
return navigationItems;
};

View File

@@ -23,28 +23,39 @@
*/
import * as React from "react";
import { Route, Link } from "react-router-dom";
import { createAttributesForTesting } from "../devBuild";
type Props = {
to: string;
label: string;
match?: string;
activeOnlyWhenExact?: boolean;
testId?: string;
};
class PrimaryNavigationLink extends React.Component<Props> {
renderLink = (route: any) => {
const { to, label } = this.props;
const { to, label, testId } = this.props;
return (
<li className={route.match ? "is-active" : ""}>
<Link to={to}>{label}</Link>
<Link to={to} {...createAttributesForTesting(testId)}>
{label}
</Link>
</li>
);
};
render() {
const { to, match, activeOnlyWhenExact } = this.props;
const { to, match, activeOnlyWhenExact, testId } = this.props;
const path = match ? match : to;
return <Route path={path} exact={activeOnlyWhenExact} children={this.renderLink} />;
return (
<Route
path={path}
exact={activeOnlyWhenExact}
children={this.renderLink}
{...createAttributesForTesting(testId)}
/>
);
}
}

View File

@@ -27,14 +27,25 @@ import classNames from "classnames";
import useMenuContext from "./MenuContext";
import { RoutingProps } from "./RoutingProps";
import useActiveMatch from "./useActiveMatch";
import { createAttributesForTesting } from "../devBuild";
type Props = RoutingProps & {
label: string;
title?: string;
icon?: string;
testId?: string;
};
const SubNavigation: FC<Props> = ({ to, activeOnlyWhenExact, activeWhenMatch, icon, title, label, children }) => {
const SubNavigation: FC<Props> = ({
to,
activeOnlyWhenExact,
activeWhenMatch,
icon,
title,
label,
children,
testId
}) => {
const context = useMenuContext();
const collapsed = context.isCollapsed();
@@ -60,7 +71,11 @@ const SubNavigation: FC<Props> = ({ to, activeOnlyWhenExact, activeWhenMatch, ic
return (
<li title={collapsed ? title : undefined}>
<Link className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")} to={to}>
<Link
className={classNames(active ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
{...createAttributesForTesting(testId)}
>
<i className={classNames(defaultIcon, "fa-fw")} /> {collapsed ? "" : label}
</Link>
{childrenList}

View File

@@ -24,6 +24,8 @@
import { Links } from "./hal";
export type AnonymousMode = "FULL" | "PROTOCOL_ONLY" | "OFF";
export type Config = {
proxyPassword: string | null;
proxyPort: number;
@@ -34,6 +36,7 @@ export type Config = {
disableGroupingGrid: boolean;
dateFormat: string;
anonymousAccessEnabled: boolean;
anonymousMode: AnonymousMode;
baseUrl: string;
forceBaseUrl: boolean;
loginAttemptLimit: number;

View File

@@ -42,7 +42,7 @@ export { AnnotatedSource, AnnotatedLine } from "./Annotate";
export { Tag } from "./Tags";
export { Config } from "./Config";
export { Config, AnonymousMode } from "./Config";
export { IndexResources } from "./IndexResources";

View File

@@ -48,6 +48,7 @@
"repositories": "Repositories",
"users": "Benutzer",
"logout": "Abmelden",
"login": "Anmelden",
"groups": "Gruppen",
"admin": "Administration"
},

View File

@@ -38,7 +38,12 @@
"realm-description": "Realm Beschreibung",
"disable-grouping-grid": "Gruppen deaktivieren",
"date-format": "Datumsformat",
"anonymous-access-enabled": "Anonyme Zugriffe erlauben",
"anonymousMode": {
"title": "Anonyme Zugriffe",
"full": "Aktivieren für Web-Oberfläche und Protokolle",
"protocolOnly": "Aktivieren für Protokolle",
"off": "Deaktivieren"
},
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin Center URL",
"enabled-xsrf-protection": "XSRF Protection aktivieren",

View File

@@ -49,6 +49,7 @@
"repositories": "Repositories",
"users": "Users",
"logout": "Logout",
"login": "Login",
"groups": "Groups",
"admin": "Administration"
},

View File

@@ -38,7 +38,12 @@
"realm-description": "Realm Description",
"disable-grouping-grid": "Disable Grouping Grid",
"date-format": "Date Format",
"anonymous-access-enabled": "Anonymous Access Enabled",
"anonymousMode": {
"title": "Anonymous Access",
"full": "Enabled for Web UI and protocols",
"protocolOnly": "Enabled for protocols",
"off": "Disabled"
},
"skip-failed-authenticators": "Skip Failed Authenticators",
"plugin-url": "Plugin Center URL",
"enabled-xsrf-protection": "Enabled XSRF Protection",

View File

@@ -63,7 +63,7 @@ class ConfigForm extends React.Component<Props, State> {
realmDescription: "",
disableGroupingGrid: false,
dateFormat: "",
anonymousAccessEnabled: false,
anonymousMode: "OFF",
baseUrl: "",
forceBaseUrl: false,
loginAttemptLimit: 0,
@@ -140,7 +140,7 @@ class ConfigForm extends React.Component<Props, State> {
realmDescription={config.realmDescription}
disableGroupingGrid={config.disableGroupingGrid}
dateFormat={config.dateFormat}
anonymousAccessEnabled={config.anonymousAccessEnabled}
anonymousMode={config.anonymousMode}
skipFailedAuthenticators={config.skipFailedAuthenticators}
pluginUrl={config.pluginUrl}
enabledXsrfProtection={config.enabledXsrfProtection}

View File

@@ -23,8 +23,8 @@
*/
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Checkbox, InputField } from "@scm-manager/ui-components";
import { NamespaceStrategies } from "@scm-manager/ui-types";
import { Checkbox, InputField, Select } from "@scm-manager/ui-components";
import { NamespaceStrategies, AnonymousMode } from "@scm-manager/ui-types";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = WithTranslation & {
@@ -32,7 +32,7 @@ type Props = WithTranslation & {
loginInfoUrl: string;
disableGroupingGrid: boolean;
dateFormat: string;
anonymousAccessEnabled: boolean;
anonymousMode: AnonymousMode;
skipFailedAuthenticators: boolean;
pluginUrl: string;
enabledXsrfProtection: boolean;
@@ -50,7 +50,7 @@ class GeneralSettings extends React.Component<Props> {
loginInfoUrl,
pluginUrl,
enabledXsrfProtection,
anonymousAccessEnabled,
anonymousMode,
namespaceStrategy,
hasUpdatePermission,
namespaceStrategies
@@ -111,13 +111,18 @@ class GeneralSettings extends React.Component<Props> {
/>
</div>
<div className="column is-half">
<Checkbox
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleEnableAnonymousAccess}
checked={anonymousAccessEnabled}
title={t("general-settings.anonymous-access-enabled")}
<Select
label={t("general-settings.anonymousMode.title")}
onChange={this.handleAnonymousMode}
value={anonymousMode}
disabled={!hasUpdatePermission}
options={[
{ label: t("general-settings.anonymousMode.full"), value: "FULL" },
{ label: t("general-settings.anonymousMode.protocolOnly"), value: "PROTOCOL_ONLY" },
{ label: t("general-settings.anonymousMode.off"), value: "OFF" }
]}
helpText={t("help.allowAnonymousAccessHelpText")}
testId={"anonymous-mode-select"}
/>
</div>
</div>
@@ -134,8 +139,8 @@ class GeneralSettings extends React.Component<Props> {
handleEnabledXsrfProtectionChange = (value: boolean) => {
this.props.onChange(true, value, "enabledXsrfProtection");
};
handleEnableAnonymousAccess = (value: boolean) => {
this.props.onChange(true, value, "anonymousAccessEnabled");
handleAnonymousMode = (value: string) => {
this.props.onChange(true, value, "anonymousMode");
};
handleNamespaceStrategyChange = (value: string) => {
this.props.onChange(true, value, "namespaceStrategy");

View File

@@ -133,6 +133,7 @@ class Admin extends React.Component<Props> {
icon="fas fa-info-circle"
label={t("admin.menu.informationNavLink")}
title={t("admin.menu.informationNavLink")}
testId="admin-information-link"
/>
{(availablePluginsLink || installedPluginsLink) && (
<SubNavigation
@@ -140,12 +141,21 @@ class Admin extends React.Component<Props> {
icon="fas fa-puzzle-piece"
label={t("plugins.menu.pluginsNavLink")}
title={t("plugins.menu.pluginsNavLink")}
testId="admin-plugins-link"
>
{installedPluginsLink && (
<NavLink to={`${url}/plugins/installed/`} label={t("plugins.menu.installedNavLink")} />
<NavLink
to={`${url}/plugins/installed/`}
label={t("plugins.menu.installedNavLink")}
testId="admin-installed-plugins-link"
/>
)}
{availablePluginsLink && (
<NavLink to={`${url}/plugins/available/`} label={t("plugins.menu.availableNavLink")} />
<NavLink
to={`${url}/plugins/available/`}
label={t("plugins.menu.availableNavLink")}
testId="admin-available-plugins-link"
/>
)}
</SubNavigation>
)}
@@ -154,6 +164,7 @@ class Admin extends React.Component<Props> {
icon="fas fa-user-shield"
label={t("repositoryRole.navLink")}
title={t("repositoryRole.navLink")}
testId="admin-repository-role-link"
activeWhenMatch={this.matchesRoles}
activeOnlyWhenExact={false}
/>
@@ -162,8 +173,13 @@ class Admin extends React.Component<Props> {
to={`${url}/settings/general`}
label={t("admin.menu.settingsNavLink")}
title={t("admin.menu.settingsNavLink")}
testId="admin-settings-link"
>
<NavLink to={`${url}/settings/general`} label={t("admin.menu.generalNavLink")} />
<NavLink
to={`${url}/settings/general`}
label={t("admin.menu.generalNavLink")}
testId="admin-settings-general-link"
/>
<ExtensionPoint name="admin.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</SecondaryNavigation>

View File

@@ -25,9 +25,8 @@ import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { PagedCollection, RepositoryRole } from "@scm-manager/ui-types";
import { CreateButton, LinkPaginator, Loading, Notification, Subtitle, Title, urls } from "@scm-manager/ui-components";
import { CreateButton, LinkPaginator, Loading, Notification, Subtitle, Title, urls, ErrorNotification } from "@scm-manager/ui-components";
import {
fetchRolesByPage,
getFetchRolesFailure,
@@ -61,8 +60,8 @@ class RepositoryRoles extends React.Component<Props> {
}
componentDidUpdate = (prevProps: Props) => {
const { loading, list, page, rolesLink, location, fetchRolesByPage } = this.props;
if (list && page && !loading) {
const { loading, error, list, page, rolesLink, location, fetchRolesByPage } = this.props;
if (list && page && !loading && !error) {
const statePage: number = list.page + 1;
if (page !== statePage || prevProps.location.search !== location.search) {
fetchRolesByPage(rolesLink, page);
@@ -71,7 +70,11 @@ class RepositoryRoles extends React.Component<Props> {
};
render() {
const { t, loading } = this.props;
const { t, loading, error } = this.props;
if (error) {
return <ErrorNotification error={error}/>;
}
if (loading) {
return <Loading />;

View File

@@ -109,16 +109,18 @@ class LoginForm extends React.Component<Props, State> {
<ErrorNotification error={this.areCredentialsInvalid()} />
<form onSubmit={this.handleSubmit}>
<InputField
testId="username-input"
placeholder={t("login.username-placeholder")}
autofocus={true}
onChange={this.handleUsernameChange}
/>
<InputField
testId="password-input"
placeholder={t("login.password-placeholder")}
type="password"
onChange={this.handlePasswordChange}
/>
<SubmitButton label={t("login.submit")} fullWidth={true} loading={loading} />
<SubmitButton label={t("login.submit")} fullWidth={true} loading={loading} testId="login-button" />
</form>
</TopMarginBox>
</div>

View File

@@ -27,7 +27,15 @@ import { connect } from "react-redux";
import { compose } from "redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { withRouter } from "react-router-dom";
import { fetchMe, getFetchMeFailure, getMe, isAuthenticated, isFetchMePending } from "../modules/auth";
import {
fetchMe,
getFetchMeFailure,
getMe,
isAuthenticated,
isFetchMePending,
isLoginPending,
isLogoutPending
} from "../modules/auth";
import { ErrorPage, Footer, Header, Loading, PrimaryNavigation } from "@scm-manager/ui-components";
import { Links, Me } from "@scm-manager/ui-types";
import {
@@ -37,6 +45,7 @@ import {
getMeLink,
isFetchIndexResourcesPending
} from "../modules/indexResource";
import Login from "./Login";
type Props = WithTranslation & {
me: Me;
@@ -64,12 +73,14 @@ class App extends Component<Props> {
let content;
const navigation = authenticated ? <PrimaryNavigation links={links} /> : "";
if (loading) {
if (!authenticated && !loading) {
content = <Login />;
} else if (loading) {
content = <Loading />;
} else if (error) {
content = <ErrorPage title={t("app.error.title")} subtitle={t("app.error.subtitle")} error={error} />;
} else {
content = <Main authenticated={authenticated} links={links} />;
content = <Main authenticated={authenticated} links={links} me={me} />;
}
return (
<div className="App">
@@ -88,9 +99,9 @@ const mapDispatchToProps = (dispatch: any) => {
};
const mapStateToProps = (state: any) => {
const authenticated = isAuthenticated(state);
const authenticated = isAuthenticated(state) && !isLogoutPending(state);
const me = getMe(state);
const loading = isFetchMePending(state) || isFetchIndexResourcesPending(state);
const loading = isFetchMePending(state) || isFetchIndexResourcesPending(state) || isLoginPending(state);
const error = getFetchMeFailure(state) || getFetchIndexResourcesFailure(state);
const links = getLinks(state);
const meLink = getMeLink(state);

View File

@@ -23,15 +23,17 @@
*/
import React from "react";
import { connect } from "react-redux";
import { Redirect, withRouter } from "react-router-dom";
import { Redirect, RouteComponentProps, withRouter } from "react-router-dom";
import { compose } from "redux";
import styled from "styled-components";
import { getLoginFailure, isAuthenticated, isLoginPending, login } from "../modules/auth";
import { getLoginFailure, getMe, isAnonymous, isLoginPending, login } from "../modules/auth";
import { getLoginInfoLink, getLoginLink } from "../modules/indexResource";
import LoginInfo from "../components/LoginInfo";
import { Me } from "@scm-manager/ui-types";
type Props = {
type Props = RouteComponentProps & {
authenticated: boolean;
me: Me;
loading: boolean;
error?: Error;
link: string;
@@ -39,10 +41,6 @@ type Props = {
// dispatcher props
login: (link: string, username: string, password: string) => void;
// context props
from: any;
location: any;
};
const HeroSection = styled.section`
@@ -65,9 +63,9 @@ class Login extends React.Component<Props> {
};
render() {
const { authenticated, ...restProps } = this.props;
const { authenticated, me, ...restProps } = this.props;
if (authenticated) {
if (authenticated && !!me) {
return this.renderRedirect();
}
@@ -86,13 +84,15 @@ class Login extends React.Component<Props> {
}
const mapStateToProps = (state: any) => {
const authenticated = isAuthenticated(state);
const authenticated = state?.auth?.me && !isAnonymous(state.auth.me);
const me = getMe(state);
const loading = isLoginPending(state);
const error = getLoginFailure(state);
const link = getLoginLink(state);
const loginInfoLink = getLoginInfoLink(state);
return {
authenticated,
me,
loading,
error,
link,

View File

@@ -22,52 +22,45 @@
* SOFTWARE.
*/
import React from "react";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { Redirect } from "react-router-dom";
import {connect} from "react-redux";
import {WithTranslation, withTranslation} from "react-i18next";
import { getLogoutFailure, isAuthenticated, isLogoutPending, isRedirecting, logout } from "../modules/auth";
import { ErrorPage, Loading } from "@scm-manager/ui-components";
import { getLogoutLink } from "../modules/indexResource";
import {getLogoutFailure, logout} from "../modules/auth";
import {ErrorPage, Loading} from "@scm-manager/ui-components";
import {getLogoutLink} from "../modules/indexResource";
import {RouteComponentProps, withRouter} from "react-router-dom";
import {compose} from "redux";
type Props = WithTranslation & {
authenticated: boolean;
loading: boolean;
redirecting: boolean;
type Props = RouteComponentProps &
WithTranslation & {
error: Error;
logoutLink: string;
// dispatcher functions
logout: (link: string) => void;
logout: (link: string, callback: () => void) => void;
};
class Logout extends React.Component<Props> {
componentDidMount() {
this.props.logout(this.props.logoutLink);
if (this.props.logoutLink) {
this.props.logout(this.props.logoutLink, () => this.props.history.push("/login"));
}
}
render() {
const { authenticated, redirecting, loading, error, t } = this.props;
const {error, t} = this.props;
if (error) {
return <ErrorPage title={t("logout.error.title")} subtitle={t("logout.error.subtitle")} error={error} />;
} else if (loading || authenticated || redirecting) {
return <Loading />;
return <ErrorPage title={t("logout.error.title")} subtitle={t("logout.error.subtitle")} error={error}/>;
} else {
return <Redirect to="/login" />;
return <Loading/>;
}
}
}
const mapStateToProps = (state: any) => {
const authenticated = isAuthenticated(state);
const loading = isLogoutPending(state);
const redirecting = isRedirecting(state);
const error = getLogoutFailure(state);
const logoutLink = getLogoutLink(state);
return {
authenticated,
loading,
redirecting,
error,
logoutLink
};
@@ -75,8 +68,8 @@ const mapStateToProps = (state: any) => {
const mapDispatchToProps = (dispatch: any) => {
return {
logout: (link: string) => dispatch(logout(link))
logout: (link: string, callback: () => void) => dispatch(logout(link, callback))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(withTranslation("commons")(Logout));
export default compose(withTranslation("commons"), withRouter, connect(mapStateToProps, mapDispatchToProps))(Logout);

View File

@@ -24,7 +24,7 @@
import React from "react";
import { Redirect, Route, Switch, withRouter } from "react-router-dom";
import { Links } from "@scm-manager/ui-types";
import { Links, Me } from "@scm-manager/ui-types";
import Overview from "../repos/containers/Overview";
import Users from "../users/containers/Users";
@@ -48,18 +48,25 @@ import Admin from "../admin/containers/Admin";
import Profile from "./Profile";
type Props = {
me: Me;
authenticated?: boolean;
links: Links;
};
class Main extends React.Component<Props> {
render() {
const { authenticated, links } = this.props;
const { authenticated, me, links } = this.props;
const redirectUrlFactory = binder.getExtension("main.redirect", this.props);
let url = "/repos/";
let url = "/";
if (authenticated) {
url = "/repos/";
}
if (redirectUrlFactory) {
url = redirectUrlFactory(this.props);
}
if (!me) {
url = "/login";
}
return (
<div className="main">
<Switch>
@@ -88,6 +95,7 @@ class Main extends React.Component<Props> {
renderAll={true}
props={{
authenticated,
me,
links
}}
/>

View File

@@ -65,6 +65,16 @@ class Profile extends React.Component<Props> {
return this.stripEndingSlash(this.props.match.url);
};
mayChangePassword = () => {
const { me } = this.props;
return !!me?._links?.password;
};
canManagePublicKeys = () => {
const { me } = this.props;
return !!me?._links?.publicKeys;
};
render() {
const url = this.matchedUrl();
@@ -94,8 +104,12 @@ class Profile extends React.Component<Props> {
<CustomQueryFlexWrappedColumns>
<PrimaryContentColumn>
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
{this.mayChangePassword() && (
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me}/>} />
)}
{this.canManagePublicKeys() && (
<Route path={`${url}/settings/publicKeys`} render={() => <SetPublicKeys user={me} />} />
)}
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
</PrimaryContentColumn>
<SecondaryNavigationColumn>
@@ -106,6 +120,7 @@ class Profile extends React.Component<Props> {
label={t("profile.informationNavLink")}
title={t("profile.informationNavLink")}
/>
{this.mayChangePassword() && (
<SubNavigation
to={`${url}/settings/password`}
label={t("profile.settingsNavLink")}
@@ -115,6 +130,7 @@ class Profile extends React.Component<Props> {
<SetPublicKeyNavLink user={me} publicKeyUrl={`${url}/settings/publicKeys`} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
)}
</SecondaryNavigation>
</SecondaryNavigationColumn>
</CustomQueryFlexWrappedColumns>

View File

@@ -24,7 +24,12 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Me } from "@scm-manager/ui-types";
import { AvatarImage, AvatarWrapper, MailLink } from "@scm-manager/ui-components";
import {
AvatarImage,
AvatarWrapper,
MailLink,
createAttributesForTesting
} from "@scm-manager/ui-components";
type Props = WithTranslation & {
me: Me;
@@ -47,11 +52,11 @@ class ProfileInfo extends React.Component<Props> {
<tbody>
<tr>
<th>{t("profile.username")}</th>
<td>{me.name}</td>
<td {...createAttributesForTesting(me.name)}>{me.name}</td>
</tr>
<tr>
<th>{t("profile.displayName")}</th>
<td>{me.displayName}</td>
<td {...createAttributesForTesting(me.displayName)}>{me.displayName}</td>
</tr>
<tr>
<th>{t("profile.mail")}</th>

View File

@@ -25,7 +25,6 @@ import React from "react";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { RouteComponentProps } from "react-router-dom";
import { History } from "history";
import { Group, PagedCollection } from "@scm-manager/ui-types";
import {
CreateButton,

View File

@@ -35,6 +35,7 @@ import reducer, {
getLoginFailure,
getLogoutFailure,
getMe,
isAnonymous,
isAuthenticated,
isFetchMePending,
isLoginPending,
@@ -349,23 +350,6 @@ describe("auth selectors", () => {
).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({
@@ -500,3 +484,8 @@ describe("auth selectors", () => {
).toBe(true);
});
});
it("should check if current user is anonymous", () => {
expect(isAnonymous({ name: "_anonymous", displayName: "Anon", _links: [], groups: [], mail: "" })).toBeTruthy();
expect(isAnonymous({ name: "scmadmin", displayName: "SCM Admin", _links: [], groups: [], mail: "" })).toBeFalsy();
});

View File

@@ -22,7 +22,7 @@
* SOFTWARE.
*/
import { Me } from "@scm-manager/ui-types";
import { Link, Me } from "@scm-manager/ui-types";
import * as types from "./types";
import { apiClient, UnauthorizedError } from "@scm-manager/ui-components";
@@ -35,6 +35,7 @@ import {
fetchIndexResourcesSuccess,
getLoginLink
} from "./indexResource";
import { AnyAction } from "redux";
// Action
@@ -61,7 +62,7 @@ const initialState = {};
export default function reducer(
state: object = initialState,
action: object = {
action: AnyAction = {
type: "UNKNOWN"
}
) {
@@ -174,7 +175,7 @@ const callFetchMe = (link: string): Promise<Me> => {
};
export const login = (loginLink: string, username: string, password: string) => {
const login_data = {
const loginData = {
cookie: true,
grant_type: "password",
username,
@@ -183,14 +184,14 @@ export const login = (loginLink: string, username: string, password: string) =>
return function(dispatch: any) {
dispatch(loginPending());
return apiClient
.post(loginLink, login_data)
.post(loginLink, loginData)
.then(() => {
dispatch(fetchIndexResourcesPending());
return callFetchIndexResources();
})
.then(response => {
dispatch(fetchIndexResourcesSuccess(response));
const meLink = response._links.me.href;
const meLink = (response._links.me as Link).href;
return callFetchMe(meLink);
})
.then(me => {
@@ -219,7 +220,7 @@ export const fetchMe = (link: string) => {
};
};
export const logout = (link: string) => {
export const logout = (link: string, callback: () => void) => {
return function(dispatch: any) {
dispatch(logoutPending());
return apiClient
@@ -247,6 +248,7 @@ export const logout = (link: string) => {
dispatch(fetchIndexResources());
}
})
.then(callback)
.catch(error => {
dispatch(logoutFailure(error));
});
@@ -256,17 +258,17 @@ export const logout = (link: string) => {
// selectors
const stateAuth = (state: object): object => {
// @ts-ignore Right types for redux not available
return state.auth || {};
};
export const isAuthenticated = (state: object) => {
if (state.auth.me && !getLoginLink(state)) {
return true;
}
return false;
// @ts-ignore Right types for redux not available
return !!((state.auth.me && !getLoginLink(state)) || isAnonymous(state.auth.me));
};
export const getMe = (state: object): Me => {
// @ts-ignore Right types for redux not available
return stateAuth(state).me;
};
@@ -295,5 +297,12 @@ export const getLogoutFailure = (state: object) => {
};
export const isRedirecting = (state: object) => {
// @ts-ignore Right types for redux not available
return !!stateAuth(state).redirecting;
};
// Helper methods
export const isAnonymous = (me: Me) => {
return me?.name === "_anonymous";
};

View File

@@ -40,6 +40,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
const newState = {};
for (const failureType in state) {
if (failureType !== identifier && !failureType.startsWith(identifier)) {
// @ts-ignore Right types not available
newState[failureType] = state[failureType];
}
}
@@ -50,6 +51,7 @@ function removeFromState(state: object, identifier: string) {
const newState = {};
for (const failureType in state) {
if (failureType !== identifier) {
// @ts-ignore Right types not available
newState[failureType] = state[failureType];
}
}
@@ -90,11 +92,13 @@ export default function reducer(
}
export function getFailure(state: object, actionType: string, itemId?: string | number) {
// @ts-ignore Right types not available
if (state.failure) {
let identifier = actionType;
if (itemId) {
identifier += "/" + itemId;
}
// @ts-ignore Right types not available
return state.failure[identifier];
}
}

View File

@@ -111,23 +111,29 @@ export function getFetchIndexResourcesFailure(state: object) {
}
export function getLinks(state: object) {
// @ts-ignore Right types not available
return state.indexResources.links;
}
export function getLink(state: object, name: string) {
// @ts-ignore Right types not available
if (state.indexResources.links && state.indexResources.links[name]) {
// @ts-ignore Right types not available
return state.indexResources.links[name].href;
}
}
export function getLinkCollection(state: object, name: string): Link[] {
// @ts-ignore Right types not available
if (state.indexResources.links && state.indexResources.links[name]) {
// @ts-ignore Right types not available
return state.indexResources.links[name];
}
return [];
}
export function getAppVersion(state: object) {
// @ts-ignore Right types not available
return state.indexResources.version;
}

View File

@@ -32,6 +32,7 @@ function removeFromState(state: object, identifier: string) {
const newState = {};
for (const childType in state) {
if (childType !== identifier) {
// @ts-ignore Right types not available
newState[childType] = state[childType];
}
}
@@ -42,6 +43,7 @@ function removeAllEntriesOfIdentifierFromState(state: object, payload: any, iden
const newState = {};
for (const childType in state) {
if (childType !== identifier && !childType.startsWith(identifier)) {
// @ts-ignore Right types not available
newState[childType] = state[childType];
}
}
@@ -92,6 +94,7 @@ export function isPending(state: object, actionType: string, itemId?: string | n
if (itemId) {
type += "/" + itemId;
}
// @ts-ignore Right types not available
if (state.pending && state.pending[type]) {
return true;
}

View File

@@ -54,6 +54,7 @@ class PermissionCheckbox extends React.Component<Props> {
checked={checked}
onChange={onChange}
disabled={disabled}
testId={label}
/>
);
}

View File

@@ -142,6 +142,7 @@ class SetPermissions extends React.Component<Props, State> {
disabled={!this.state.permissionsChanged}
loading={loading}
label={t("setPermissions.button")}
testId="set-permissions-button"
/>
}
/>

View File

@@ -25,7 +25,6 @@ import React from "react";
import { connect } from "react-redux";
import { RouteComponentProps, withRouter } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { RepositoryCollection } from "@scm-manager/ui-types";
import {
CreateButton,
@@ -77,11 +76,17 @@ class Overview extends React.Component<Props> {
render() {
const { error, loading, showCreateButton, t } = this.props;
return (
<Page title={t("overview.title")} subtitle={t("overview.subtitle")} loading={loading} error={error}>
{this.renderOverview()}
<PageActions>
<OverviewPageActions showCreateButton={showCreateButton} link="repos" label={t("overview.createButton")} />
<OverviewPageActions
showCreateButton={showCreateButton}
link="repos"
label={t("overview.createButton")}
testId="repository-overview"
/>
</PageActions>
</Page>
);

View File

@@ -42,7 +42,7 @@ class EditUserNavLink extends React.Component<Props> {
if (!this.isEditable()) {
return null;
}
return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} />;
return <NavLink to={editUrl} label={t("singleUser.menu.generalNavLink")} testId="user-edit-link" />;
}
}

View File

@@ -38,7 +38,7 @@ class ChangePasswordNavLink extends React.Component<Props> {
if (!this.hasPermissionToSetPassword()) {
return null;
}
return <NavLink to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} />;
return <NavLink to={passwordUrl} label={t("singleUser.menu.setPasswordNavLink")} testId="user-password-link"/>;
}
hasPermissionToSetPassword = () => {

View File

@@ -38,7 +38,7 @@ class ChangePermissionNavLink extends React.Component<Props> {
if (!this.hasPermissionToSetPermission()) {
return null;
}
return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} />;
return <NavLink to={permissionsUrl} label={t("singleUser.menu.setPermissionsNavLink")} testId="user-permissions-link"/>;
}
hasPermissionToSetPermission = () => {

View File

@@ -24,7 +24,12 @@
import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { User } from "@scm-manager/ui-types";
import { Checkbox, DateFromNow, MailLink } from "@scm-manager/ui-components";
import {
Checkbox,
DateFromNow,
MailLink,
createAttributesForTesting
} from "@scm-manager/ui-components";
type Props = WithTranslation & {
user: User;
@@ -38,11 +43,11 @@ class Details extends React.Component<Props> {
<tbody>
<tr>
<th>{t("user.name")}</th>
<td>{user.name}</td>
<td {...createAttributesForTesting(user.name)}>{user.name}</td>
</tr>
<tr>
<th>{t("user.displayName")}</th>
<td>{user.displayName}</td>
<td {...createAttributesForTesting(user.displayName)}>{user.displayName}</td>
</tr>
<tr>
<th>{t("user.mail")}</th>

View File

@@ -25,7 +25,7 @@ import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { User } from "@scm-manager/ui-types";
import { Icon } from "@scm-manager/ui-components";
import { Icon, createAttributesForTesting } from "@scm-manager/ui-components";
type Props = WithTranslation & {
user: User;
@@ -33,7 +33,11 @@ type Props = WithTranslation & {
class UserRow extends React.Component<Props> {
renderLink(to: string, label: string) {
return <Link to={to}>{label}</Link>;
return (
<Link to={to} {...createAttributesForTesting(label)}>
{label}
</Link>
);
}
render() {

View File

@@ -120,11 +120,13 @@ class SingleUser extends React.Component<Props> {
icon="fas fa-info-circle"
label={t("singleUser.menu.informationNavLink")}
title={t("singleUser.menu.informationNavLink")}
testId="user-information-link"
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
title={t("singleUser.menu.settingsNavLink")}
testId="user-settings-link"
>
<EditUserNavLink user={user} editUrl={`${url}/settings/general`} />
<SetPasswordNavLink user={user} passwordUrl={`${url}/settings/password`} />

View File

@@ -25,7 +25,6 @@ import React from "react";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { RouteComponentProps } from "react-router-dom";
import { History } from "history";
import { PagedCollection, User } from "@scm-manager/ui-types";
import {
CreateButton,
@@ -88,6 +87,7 @@ class Users extends React.Component<Props> {
render() {
const { users, loading, error, canAddUsers, t } = this.props;
return (
<Page title={t("users.title")} subtitle={t("users.subtitle")} loading={loading || !users} error={error}>
{this.renderUserTable()}

View File

@@ -29,6 +29,7 @@ import de.otto.edison.hal.Links;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import sonia.scm.security.AnonymousMode;
import java.util.Set;
@@ -46,6 +47,7 @@ public class ConfigDto extends HalRepresentation {
private boolean disableGroupingGrid;
private String dateFormat;
private boolean anonymousAccessEnabled;
private AnonymousMode anonymousMode;
private String baseUrl;
private boolean forceBaseUrl;
private int loginAttemptLimit;

View File

@@ -24,8 +24,11 @@
package sonia.scm.api.v2.resources;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection.
@SuppressWarnings("squid:S3306")
@@ -33,4 +36,15 @@ import sonia.scm.config.ScmConfiguration;
public abstract class ConfigDtoToScmConfigurationMapper {
public abstract ScmConfiguration map(ConfigDto dto);
@AfterMapping // Should map anonymous mode from old flag if not send explicit
void mapAnonymousMode(@MappingTarget ScmConfiguration config, ConfigDto configDto) {
if (configDto.getAnonymousMode() == null) {
if (configDto.isAnonymousAccessEnabled()) {
config.setAnonymousMode(AnonymousMode.PROTOCOL_ONLY);
} else {
config.setAnonymousMode(AnonymousMode.OFF);
}
}
}
}

View File

@@ -31,6 +31,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import sonia.scm.group.Group;
import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupPermissions;
import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.web.VndMediaType;
@@ -106,6 +107,7 @@ public class GroupCollectionResource {
@QueryParam("desc") boolean desc,
@DefaultValue("") @QueryParam("q") String search
) {
GroupPermissions.list().check();
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult));
}

View File

@@ -35,6 +35,7 @@ import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.group.GroupPermissions;
import sonia.scm.plugin.PluginPermissions;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.Authentications;
import sonia.scm.security.PermissionPermissions;
import sonia.scm.user.UserPermissions;
@@ -70,7 +71,7 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.single(link("loginInfo", loginInfoUrl));
}
if (SecurityUtils.getSubject().isAuthenticated()) {
if (shouldAppendSubjectRelatedLinks()) {
builder.single(link("me", resourceLinks.me().self()));
if (Authentications.isAuthenticatedSubjectAnonymous()) {
@@ -120,4 +121,19 @@ public class IndexDtoGenerator extends HalAppenderMapper {
return new IndexDto(builder.build(), embeddedBuilder.build(), scmContextProvider.getVersion());
}
private boolean shouldAppendSubjectRelatedLinks() {
return isAuthenticatedSubjectNotAnonymous()
|| isAuthenticatedSubjectAllowedToBeAnonymous();
}
private boolean isAuthenticatedSubjectAllowedToBeAnonymous() {
return Authentications.isAuthenticatedSubjectAnonymous()
&& configuration.getAnonymousMode() == AnonymousMode.FULL;
}
private boolean isAuthenticatedSubjectNotAnonymous() {
return SecurityUtils.getSubject().isAuthenticated()
&& !Authentications.isAuthenticatedSubjectAnonymous();
}
}

View File

@@ -30,7 +30,6 @@ 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;
@@ -92,7 +91,7 @@ public class MeDtoFactory extends HalAppenderMapper {
if (UserPermissions.changePublicKeys(user).isPermitted()) {
linksBuilder.single(link("publicKeys", resourceLinks.user().publicKeys(user.getName())));
}
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted() && !Authentications.isSubjectAnonymous(user.getName())) {
if (userManager.isTypeDefault(user) && UserPermissions.changePassword(user).isPermitted()) {
linksBuilder.single(link("password", resourceLinks.me().passwordChange()));
}
@@ -101,5 +100,4 @@ public class MeDtoFactory extends HalAppenderMapper {
return new MeDto(linksBuilder.build(), embeddedBuilder.build());
}
}

View File

@@ -27,9 +27,12 @@ package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import org.mapstruct.AfterMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import javax.inject.Inject;
@@ -44,6 +47,15 @@ public abstract class ScmConfigurationToConfigDtoMapper extends BaseMapper<ScmCo
@Inject
private ResourceLinks resourceLinks;
@Mapping(target = "anonymousAccessEnabled", source = "anonymousMode", qualifiedByName = "mapAnonymousAccess")
@Mapping(target = "attributes", ignore = true)
public abstract ConfigDto map(ScmConfiguration scmConfiguration);
@Named("mapAnonymousAccess")
boolean mapAnonymousAccess(AnonymousMode anonymousMode) {
return anonymousMode != AnonymousMode.OFF;
}
@AfterMapping
void appendLinks(ScmConfiguration config, @MappingTarget ConfigDto target) {
Links.Builder linksBuilder = linkingTo().self(resourceLinks.config().self());

View File

@@ -34,6 +34,7 @@ import sonia.scm.search.SearchRequest;
import sonia.scm.search.SearchUtil;
import sonia.scm.user.User;
import sonia.scm.user.UserManager;
import sonia.scm.user.UserPermissions;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
@@ -108,6 +109,7 @@ public class UserCollectionResource {
@DefaultValue("false") @QueryParam("desc") boolean desc,
@DefaultValue("") @QueryParam("q") String search
) {
UserPermissions.list().check();
return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc,
pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult));
}

View File

@@ -33,6 +33,7 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.Priority;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import sonia.scm.web.filter.HttpFilter;
import sonia.scm.web.filter.PropagatePrincipleServletRequestWrapper;
@@ -89,7 +90,7 @@ public class PropagatePrincipleFilter extends HttpFilter
private boolean hasPermission(Subject subject)
{
return ((configuration != null)
&& configuration.isAnonymousAccessEnabled()) || subject.isAuthenticated()
&& configuration.getAnonymousMode() != AnonymousMode.OFF) || subject.isAuthenticated()
|| subject.isRemembered();
}

View File

@@ -31,6 +31,7 @@ import org.slf4j.LoggerFactory;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
@@ -94,7 +95,7 @@ public class SetupContextListener implements ServletContextListener {
}
private boolean anonymousUserRequiredButNotExists() {
return scmConfiguration.isAnonymousAccessEnabled() && !userManager.contains(SCMContext.USER_ANONYMOUS);
return scmConfiguration.getAnonymousMode() != AnonymousMode.OFF && !userManager.contains(SCMContext.USER_ANONYMOUS);
}
private boolean shouldCreateAdminAccount() {

View File

@@ -92,6 +92,7 @@ public class BearerRealm extends AuthenticatingRealm
checkArgument(token instanceof BearerToken, "%s is required", BearerToken.class);
BearerToken bt = (BearerToken) token;
AccessToken accessToken = tokenResolver.resolve(bt);
return helper.authenticationInfoBuilder(accessToken.getSubject())

View File

@@ -25,15 +25,17 @@
package sonia.scm.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import java.util.Set;
import javax.inject.Inject;
import org.apache.shiro.authc.AuthenticationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import java.util.Set;
/**
* Jwt implementation of {@link AccessTokenResolver}.
*
@@ -71,6 +73,8 @@ public final class JwtAccessTokenResolver implements AccessTokenResolver {
validate(token);
return token;
} catch (ExpiredJwtException ex) {
throw new TokenExpiredException("The jwt token has been expired", ex);
} catch (JwtException ex) {
throw new AuthenticationException("signature is invalid", ex);
}

View File

@@ -0,0 +1,104 @@
/*
* 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.update.repository;
import lombok.Getter;
import lombok.NoArgsConstructor;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.migration.UpdateStep;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AnonymousMode;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.ConfigurationStoreFactory;
import sonia.scm.store.StoreConstants;
import sonia.scm.version.Version;
import javax.inject.Inject;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.nio.file.Path;
import java.nio.file.Paths;
import static sonia.scm.version.Version.parse;
@Extension
public class AnonymousModeUpdateStep implements UpdateStep {
private final SCMContextProvider contextProvider;
private final ConfigurationStore<ScmConfiguration> configStore;
@Inject
public AnonymousModeUpdateStep(SCMContextProvider contextProvider, ConfigurationStoreFactory configurationStoreFactory) {
this.contextProvider = contextProvider;
this.configStore = configurationStoreFactory.withType(ScmConfiguration.class).withName("config").build();
}
@Override
public void doUpdate() throws JAXBException {
Path configFile = determineConfigDirectory().resolve("config" + StoreConstants.FILE_EXTENSION);
if (configFile.toFile().exists()) {
PreUpdateScmConfiguration oldConfig = getPreUpdateScmConfigurationFromOldConfig(configFile);
ScmConfiguration config = configStore.get();
if (oldConfig.isAnonymousAccessEnabled()) {
config.setAnonymousMode(AnonymousMode.PROTOCOL_ONLY);
} else {
config.setAnonymousMode(AnonymousMode.OFF);
}
configStore.set(config);
}
}
@Override
public Version getTargetVersion() {
return parse("2.4.0");
}
@Override
public String getAffectedDataType() {
return "config.xml";
}
private PreUpdateScmConfiguration getPreUpdateScmConfigurationFromOldConfig(Path configFile) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(AnonymousModeUpdateStep.PreUpdateScmConfiguration.class);
return (AnonymousModeUpdateStep.PreUpdateScmConfiguration) jaxbContext.createUnmarshaller().unmarshal(configFile.toFile());
}
private Path determineConfigDirectory() {
return contextProvider.resolve(Paths.get(StoreConstants.CONFIG_DIRECTORY_NAME));
}
@XmlRootElement(name = "scm-config")
@XmlAccessorType(XmlAccessType.FIELD)
@NoArgsConstructor
@Getter
static class PreUpdateScmConfiguration {
private boolean anonymousAccessEnabled;
}
}

View File

@@ -31,6 +31,7 @@ import sonia.scm.HandlerEventType;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.plugin.Extension;
import sonia.scm.security.AnonymousMode;
import javax.inject.Inject;
@@ -38,7 +39,7 @@ import javax.inject.Inject;
@Extension
public class AnonymousUserDeletionEventHandler {
private ScmConfiguration scmConfiguration;
private final ScmConfiguration scmConfiguration;
@Inject
public AnonymousUserDeletionEventHandler(ScmConfiguration scmConfiguration) {
@@ -55,6 +56,6 @@ public class AnonymousUserDeletionEventHandler {
private boolean isAnonymousUserDeletionNotAllowed(UserEvent event) {
return event.getEventType() == HandlerEventType.BEFORE_DELETE
&& event.getItem().getName().equals(SCMContext.USER_ANONYMOUS)
&& scmConfiguration.isAnonymousAccessEnabled();
&& scmConfiguration.getAnonymousMode() != AnonymousMode.OFF;
}
}

View File

@@ -67,7 +67,7 @@ public class AuthenticationResourceTest {
@Rule
public ShiroRule shiro = new ShiroRule();
private RestDispatcher dispatcher = new RestDispatcher();
private final RestDispatcher dispatcher = new RestDispatcher();
@Mock
private AccessTokenBuilderFactory accessTokenBuilderFactory;
@@ -75,9 +75,9 @@ public class AuthenticationResourceTest {
@Mock
private AccessTokenBuilder accessTokenBuilder;
private AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
private final AccessTokenCookieIssuer cookieIssuer = new DefaultAccessTokenCookieIssuer(mock(ScmConfiguration.class));
private MockHttpResponse response = new MockHttpResponse();
private final MockHttpResponse response = new MockHttpResponse();
private static final String AUTH_JSON_TRILLIAN = "{\n" +
"\t\"cookie\": true,\n" +

View File

@@ -29,6 +29,7 @@ import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.internal.util.collections.Sets;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.util.Arrays;
@@ -41,9 +42,9 @@ public class ConfigDtoToScmConfigurationMapperTest {
@InjectMocks
private ConfigDtoToScmConfigurationMapperImpl mapper;
private String[] expectedUsers = { "trillian", "arthur" };
private String[] expectedGroups = { "admin", "plebs" };
private String[] expectedExcludes = { "ex", "clude" };
private String[] expectedUsers = {"trillian", "arthur"};
private String[] expectedGroups = {"admin", "plebs"};
private String[] expectedExcludes = {"ex", "clude"};
@Before
public void init() {
@@ -55,27 +56,42 @@ public class ConfigDtoToScmConfigurationMapperTest {
ConfigDto dto = createDefaultDto();
ScmConfiguration config = mapper.map(dto);
assertEquals("prPw" , config.getProxyPassword());
assertEquals(42 , config.getProxyPort());
assertEquals("srvr" , config.getProxyServer());
assertEquals("user" , config.getProxyUser());
assertEquals("prPw", config.getProxyPassword());
assertEquals(42, config.getProxyPort());
assertEquals("srvr", config.getProxyServer());
assertEquals("user", config.getProxyUser());
assertTrue(config.isEnableProxy());
assertEquals("realm" , config.getRealmDescription());
assertEquals("realm", config.getRealmDescription());
assertTrue(config.isDisableGroupingGrid());
assertEquals("yyyy" , config.getDateFormat());
assertTrue(config.isAnonymousAccessEnabled());
assertEquals("baseurl" , config.getBaseUrl());
assertEquals("yyyy", config.getDateFormat());
assertEquals(AnonymousMode.PROTOCOL_ONLY, config.getAnonymousMode());
assertEquals("baseurl", config.getBaseUrl());
assertTrue(config.isForceBaseUrl());
assertEquals(41 , config.getLoginAttemptLimit());
assertEquals(41, config.getLoginAttemptLimit());
assertTrue("proxyExcludes", config.getProxyExcludes().containsAll(Arrays.asList(expectedExcludes)));
assertTrue(config.isSkipFailedAuthenticators());
assertEquals("https://plug.ins" , config.getPluginUrl());
assertEquals(40 , config.getLoginAttemptLimitTimeout());
assertEquals("https://plug.ins", config.getPluginUrl());
assertEquals(40, config.getLoginAttemptLimitTimeout());
assertTrue(config.isEnabledXsrfProtection());
assertEquals("username", config.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", config.getLoginInfoUrl());
}
@Test
public void shouldMapAnonymousAccessFieldToAnonymousMode() {
ConfigDto dto = createDefaultDto();
ScmConfiguration config = mapper.map(dto);
assertEquals(AnonymousMode.PROTOCOL_ONLY, config.getAnonymousMode());
dto.setAnonymousMode(null);
dto.setAnonymousAccessEnabled(false);
ScmConfiguration config2 = mapper.map(dto);
assertEquals(AnonymousMode.OFF, config2.getAnonymousMode());
}
private ConfigDto createDefaultDto() {
ConfigDto configDto = new ConfigDto();
configDto.setProxyPassword("prPw");
@@ -86,7 +102,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setRealmDescription("realm");
configDto.setDisableGroupingGrid(true);
configDto.setDateFormat("yyyy");
configDto.setAnonymousAccessEnabled(true);
configDto.setAnonymousMode(AnonymousMode.PROTOCOL_ONLY);
configDto.setBaseUrl("baseurl");
configDto.setForceBaseUrl(true);
configDto.setLoginAttemptLimit(41);

View File

@@ -0,0 +1,137 @@
/*
* MIT License
*
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package sonia.scm.api.v2.resources;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.BasicContextProvider;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.net.URI;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
import static sonia.scm.SCMContext.USER_ANONYMOUS;
@ExtendWith(MockitoExtension.class)
class IndexDtoGeneratorTest {
private static final ScmPathInfo scmPathInfo = () -> URI.create("/api/v2");
@Mock
private ScmConfiguration configuration;
@Mock
private BasicContextProvider contextProvider;
@Mock
private ResourceLinks resourceLinks;
@Mock
private Subject subject;
@InjectMocks
private IndexDtoGenerator generator;
@BeforeEach
void bindSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void tearDownSubject() {
ThreadContext.unbindSubject();
}
@Test
void shouldAppendMeIfAuthenticated() {
mockSubjectRelatedResourceLinks();
when(subject.isAuthenticated()).thenReturn(true);
when(contextProvider.getVersion()).thenReturn("2.x");
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
}
@Test
void shouldNotAppendMeIfUserIsAuthenticatedButAnonymous() {
mockResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
}
@Test
void shouldAppendMeIfUserIsAnonymousAndAnonymousModeIsFullEnabled() {
mockSubjectRelatedResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isPresent();
}
@Test
void shouldNotAppendMeIfUserIsAnonymousAndAnonymousModeIsProtocolOnly() {
mockResourceLinks();
when(subject.getPrincipal()).thenReturn(USER_ANONYMOUS);
when(subject.isAuthenticated()).thenReturn(true);
when(configuration.getAnonymousMode()).thenReturn(AnonymousMode.PROTOCOL_ONLY);
IndexDto dto = generator.generate();
assertThat(dto.getLinks().getLinkBy("me")).isNotPresent();
}
private void mockResourceLinks() {
when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(scmPathInfo));
when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(scmPathInfo));
when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(scmPathInfo));
}
private void mockSubjectRelatedResourceLinks() {
mockResourceLinks();
when(resourceLinks.repositoryCollection()).thenReturn(new ResourceLinks.RepositoryCollectionLinks(scmPathInfo));
when(resourceLinks.repositoryVerbs()).thenReturn(new ResourceLinks.RepositoryVerbLinks(scmPathInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(scmPathInfo));
when(resourceLinks.repositoryRoleCollection()).thenReturn(new ResourceLinks.RepositoryRoleCollectionLinks(scmPathInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(scmPathInfo));
when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(scmPathInfo, new ResourceLinks.UserLinks(scmPathInfo)));
}
}

View File

@@ -187,15 +187,15 @@ class MeDtoFactoryTest {
}
@Test
void shouldNotGetPasswordLinkForAnonymousUser() {
void shouldAppendOnlySelfLinkIfAnonymousUser() {
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("self")).isPresent();
assertThat(dto.getLinks().getLinkBy("password")).isNotPresent();
assertThat(dto.getLinks().getLinkBy("delete")).isNotPresent();
assertThat(dto.getLinks().getLinkBy("update")).isNotPresent();
}
@Test
@@ -235,6 +235,4 @@ class MeDtoFactoryTest {
MeDto dto = meDtoFactory.create();
assertThat(dto.getLinks().getLinkBy("profile").get().getHref()).isEqualTo("http://hitchhiker.com/users/trillian");
}
}

View File

@@ -34,12 +34,14 @@ import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.internal.util.collections.Sets;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import java.net.URI;
import java.util.Arrays;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -49,9 +51,9 @@ public class ScmConfigurationToConfigDtoMapperTest {
private URI baseUri = URI.create("http://example.com/base/");
private String[] expectedUsers = { "trillian", "arthur" };
private String[] expectedGroups = { "admin", "plebs" };
private String[] expectedExcludes = { "ex", "clude" };
private String[] expectedUsers = {"trillian", "arthur"};
private String[] expectedGroups = {"admin", "plebs"};
private String[] expectedExcludes = {"ex", "clude"};
@SuppressWarnings("unused") // Is injected
private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@@ -85,22 +87,22 @@ public class ScmConfigurationToConfigDtoMapperTest {
when(subject.isPermitted("configuration:write:global")).thenReturn(true);
ConfigDto dto = mapper.map(config);
assertEquals("heartOfGold" , dto.getProxyPassword());
assertEquals(1234 , dto.getProxyPort());
assertEquals("proxyserver" , dto.getProxyServer());
assertEquals("trillian" , dto.getProxyUser());
assertEquals("heartOfGold", dto.getProxyPassword());
assertEquals(1234, dto.getProxyPort());
assertEquals("proxyserver", dto.getProxyServer());
assertEquals("trillian", dto.getProxyUser());
assertTrue(dto.isEnableProxy());
assertEquals("description" , dto.getRealmDescription());
assertEquals("description", dto.getRealmDescription());
assertTrue(dto.isDisableGroupingGrid());
assertEquals("dd" , dto.getDateFormat());
assertTrue(dto.isAnonymousAccessEnabled());
assertEquals("baseurl" , dto.getBaseUrl());
assertEquals("dd", dto.getDateFormat());
assertSame(AnonymousMode.FULL, dto.getAnonymousMode());
assertEquals("baseurl", dto.getBaseUrl());
assertTrue(dto.isForceBaseUrl());
assertEquals(1 , dto.getLoginAttemptLimit());
assertEquals(1, dto.getLoginAttemptLimit());
assertTrue("proxyExcludes", dto.getProxyExcludes().containsAll(Arrays.asList(expectedExcludes)));
assertTrue(dto.isSkipFailedAuthenticators());
assertEquals("pluginurl" , dto.getPluginUrl());
assertEquals(2 , dto.getLoginAttemptLimitTimeout());
assertEquals("pluginurl", dto.getPluginUrl());
assertEquals(2, dto.getLoginAttemptLimitTimeout());
assertTrue(dto.isEnabledXsrfProtection());
assertEquals("username", dto.getNamespaceStrategy());
assertEquals("https://scm-manager.org/login-info", dto.getLoginInfoUrl());
@@ -121,6 +123,21 @@ public class ScmConfigurationToConfigDtoMapperTest {
assertFalse(dto.getLinks().hasLink("update"));
}
@Test
public void shouldMapAnonymousAccessField() {
ScmConfiguration config = createConfiguration();
when(subject.hasRole("configuration:write:global")).thenReturn(false);
ConfigDto dto = mapper.map(config);
assertTrue(dto.isAnonymousAccessEnabled());
config.setAnonymousMode(AnonymousMode.OFF);
ConfigDto secondDto = mapper.map(config);
assertFalse(secondDto.isAnonymousAccessEnabled());
}
private ScmConfiguration createConfiguration() {
ScmConfiguration config = new ScmConfiguration();
config.setProxyPassword("heartOfGold");
@@ -131,7 +148,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
config.setRealmDescription("description");
config.setDisableGroupingGrid(true);
config.setDateFormat("dd");
config.setAnonymousAccessEnabled(true);
config.setAnonymousMode(AnonymousMode.FULL);
config.setBaseUrl("baseurl");
config.setForceBaseUrl(true);
config.setLoginAttemptLimit(1);

View File

@@ -38,6 +38,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import sonia.scm.user.User;
import sonia.scm.user.UserTestData;
@@ -110,7 +111,7 @@ public class PropagatePrincipleFilterTest {
*/
@Test
public void testAnonymousWithAccessEnabled() throws IOException, ServletException {
configuration.setAnonymousAccessEnabled(true);
configuration.setAnonymousMode(AnonymousMode.FULL);
// execute
propagatePrincipleFilter.doFilter(request, response, chain);

View File

@@ -37,6 +37,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import sonia.scm.security.PermissionAssigner;
import sonia.scm.security.PermissionDescriptor;
import sonia.scm.user.User;
@@ -82,7 +83,7 @@ class SetupContextListenerTest {
@BeforeEach
void mockScmConfiguration() {
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(false);
when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.OFF);
}
@BeforeEach
@@ -145,7 +146,7 @@ class SetupContextListenerTest {
void shouldCreateAnonymousUserIfRequired() {
List<User> users = Lists.newArrayList(UserTestData.createTrillian());
when(userManager.getAll()).thenReturn(users);
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true);
when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
setupContextListener.contextInitialized(null);
@@ -166,7 +167,7 @@ class SetupContextListenerTest {
void shouldNotCreateAnonymousUserIfAlreadyExists() {
List<User> users = Lists.newArrayList(SCMContext.ANONYMOUS);
when(userManager.getAll()).thenReturn(users);
when(scmConfiguration.isAnonymousAccessEnabled()).thenReturn(true);
when(scmConfiguration.getAnonymousMode()).thenReturn(AnonymousMode.FULL);
setupContextListener.contextInitialized(null);

View File

@@ -130,7 +130,7 @@ public class JwtAccessTokenResolverTest {
String compact = createCompactToken("trillian", secureKey, exp, Scope.empty());
// expect exception
expectedException.expect(AuthenticationException.class);
expectedException.expect(TokenExpiredException.class);
expectedException.expectCause(instanceOf(ExpiredJwtException.class));
BearerToken bearer = BearerToken.valueOf(compact);

View File

@@ -0,0 +1,104 @@
/*
* 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.update.repository;
import com.google.common.io.Resources;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.SCMContextProvider;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import sonia.scm.store.ConfigurationStore;
import sonia.scm.store.InMemoryConfigurationStoreFactory;
import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static sonia.scm.store.InMemoryConfigurationStoreFactory.create;
@ExtendWith(MockitoExtension.class)
class AnonymousModeUpdateStepTest {
@Mock
private SCMContextProvider contextProvider;
private AnonymousModeUpdateStep updateStep;
private ConfigurationStore<ScmConfiguration> configurationStore;
private Path configDir;
@BeforeEach
void initUpdateStep(@TempDir Path tempDir) throws IOException {
when(contextProvider.resolve(any(Path.class))).thenReturn(tempDir.toAbsolutePath());
configDir = tempDir;
Files.createDirectories(configDir);
InMemoryConfigurationStoreFactory inMemoryConfigurationStoreFactory = create();
configurationStore = inMemoryConfigurationStoreFactory.get("config", null);
updateStep = new AnonymousModeUpdateStep(contextProvider, inMemoryConfigurationStoreFactory);
}
@Test
void shouldNotUpdateIfConfigFileNotAvailable() throws JAXBException {
updateStep.doUpdate();
assertThat(configurationStore.getOptional()).isNotPresent();
}
@Test
void shouldUpdateDisabledAnonymousMode() throws JAXBException, IOException {
copyTestDatabaseFile(configDir, "config.xml", "config.xml");
configurationStore.set(new ScmConfiguration());
updateStep.doUpdate();
assertThat((configurationStore.get()).getAnonymousMode()).isEqualTo(AnonymousMode.OFF);
}
@Test
void shouldUpdateEnabledAnonymousMode() throws JAXBException, IOException {
copyTestDatabaseFile(configDir, "config-withAnon.xml", "config.xml");
configurationStore.set(new ScmConfiguration());
updateStep.doUpdate();
assertThat((configurationStore.get()).getAnonymousMode()).isEqualTo(AnonymousMode.PROTOCOL_ONLY);
}
private void copyTestDatabaseFile(Path configDir, String sourceFileName, String targetFileName) throws IOException {
URL url = Resources.getResource("sonia/scm/update/security/" + sourceFileName);
Files.copy(url.openStream(), configDir.resolve(targetFileName));
}
}

View File

@@ -29,6 +29,7 @@ import org.junit.jupiter.api.Test;
import sonia.scm.HandlerEventType;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.AnonymousMode;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -45,7 +46,7 @@ class AnonymousUserDeletionEventHandlerTest {
@Test
void shouldThrowAnonymousUserDeletionExceptionIfAnonymousAccessIsEnabled() {
scmConfiguration.setAnonymousAccessEnabled(true);
scmConfiguration.setAnonymousMode(AnonymousMode.FULL);
hook = new AnonymousUserDeletionEventHandler(scmConfiguration);
UserEvent deletionEvent = new UserEvent(HandlerEventType.BEFORE_DELETE, SCMContext.ANONYMOUS);
@@ -55,7 +56,7 @@ class AnonymousUserDeletionEventHandlerTest {
@Test
void shouldNotThrowAnonymousUserDeletionException() {
scmConfiguration.setAnonymousAccessEnabled(false);
scmConfiguration.setAnonymousMode(AnonymousMode.OFF);
hook = new AnonymousUserDeletionEventHandler(scmConfiguration);
UserEvent deletionEvent = new UserEvent(HandlerEventType.BEFORE_DELETE, SCMContext.ANONYMOUS);

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
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.
-->
<scm-config>
<admin-groups>admins,vogons</admin-groups>
<admin-users>arthur,dent,ldap-admin</admin-users>
<base-url>http://localhost:8081/scm</base-url>
<enableProxy>false</enableProxy>
<force-base-url>false</force-base-url>
<forwardPort>80</forwardPort>
<plugin-url>http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&amp;arch={arch}&amp;snapshot=false</plugin-url>
<proxyPort>8080</proxyPort>
<proxyServer>proxy.mydomain.com</proxyServer>
<servername>localhost</servername>
<enableSSL>false</enableSSL>
<enablePortForward>false</enablePortForward>
<sslPort>8181</sslPort>
<disableGroupingGrid>false</disableGroupingGrid>
<dateFormat>Y-m-d H:i:s</dateFormat>
<anonymousAccessEnabled>true</anonymousAccessEnabled>
</scm-config>

465
yarn.lock
View File

@@ -1070,6 +1070,50 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
"@cypress/listr-verbose-renderer@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@cypress/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#a77492f4b11dcc7c446a34b3e28721afd33c642a"
integrity sha1-p3SS9LEdzHxEajSz4ochr9M8ZCo=
dependencies:
chalk "^1.1.3"
cli-cursor "^1.0.2"
date-fns "^1.27.2"
figures "^1.7.0"
"@cypress/request@^2.88.5":
version "2.88.5"
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.5.tgz#8d7ecd17b53a849cfd5ab06d5abe7d84976375d7"
integrity sha512-TzEC1XMi1hJkywWpRfD2clreTa/Z+lOrXDCxxBTBPEcY5azdPi56A6Xw+O4tWJnaJH3iIE7G5aDXZC6JgRZLcA==
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.3"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.5.0"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
"@cypress/xvfb@^1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a"
integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==
dependencies:
debug "^3.1.0"
lodash.once "^4.1.1"
"@emotion/babel-utils@^0.6.4":
version "0.6.10"
resolved "https://registry.yarnpkg.com/@emotion/babel-utils/-/babel-utils-0.6.10.tgz#83dbf3dfa933fae9fc566e54fbb45f14674c6ccc"
@@ -2460,6 +2504,13 @@
prop-types "^15.6.1"
react-lifecycles-compat "^3.0.4"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
integrity sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==
dependencies:
any-observable "^0.3.0"
"@sinonjs/commons@^1.7.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
@@ -3298,6 +3349,16 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/sinonjs__fake-timers@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.1.tgz#681df970358c82836b42f989188d133e218c458e"
integrity sha512-yYezQwGWty8ziyYLdZjwxyMb0CZR49h8JALHGrxjQHWlqGgc8kLdHEgWrgL0uZ29DMvEVBDnHU2Wg36zKSIUtA==
"@types/sizzle@^2.3.2":
version "2.3.2"
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -3850,6 +3911,11 @@ ansi-to-html@^0.6.11:
dependencies:
entities "^1.1.2"
any-observable@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b"
integrity sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
@@ -3886,6 +3952,11 @@ aproba@^2.0.0:
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
arch@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/arch/-/arch-2.1.2.tgz#0c52bbe7344bb4fa260c443d2cbad9c00ff2f0bf"
integrity sha512-NTBIIbAfkJeIletyABbVtdPgeKfDafR+1mZV/AyyfC1UkVkp9iUjV+wwmqtUgphHYajbI86jejBJp5e+jkGTiQ==
are-we-there-yet@~1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
@@ -4111,6 +4182,11 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
async@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -4163,11 +4239,6 @@ babel-code-frame@^6.22.0:
esutils "^2.0.2"
js-tokens "^3.0.2"
babel-core@7.0.0-bridge.0:
version "7.0.0-bridge.0"
resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece"
integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==
babel-eslint@^10.0.3:
version "10.1.0"
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
@@ -4688,7 +4759,7 @@ bindings@^1.5.0:
dependencies:
file-uri-to-path "1.0.0"
bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5:
bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -4890,6 +4961,11 @@ btoa-lite@^1.0.0:
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc=
buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@@ -5019,6 +5095,11 @@ cache-base@^1.0.1:
union-value "^1.0.0"
unset-value "^1.0.0"
cachedir@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8"
integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==
call-me-maybe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b"
@@ -5153,7 +5234,7 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^1.1.3:
chalk@^1.0.0, chalk@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=
@@ -5210,6 +5291,11 @@ chardet@^0.7.0:
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==
check-more-types@^2.24.0:
version "2.24.0"
resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600"
integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=
cheerio@^1.0.0-rc.3:
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
@@ -5313,7 +5399,14 @@ cli-boxes@^2.2.0:
resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d"
integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w==
cli-cursor@^2.1.0:
cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=
dependencies:
restore-cursor "^1.0.1"
cli-cursor@^2.0.0, cli-cursor@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5"
integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=
@@ -5327,7 +5420,7 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
cli-table3@0.5.1:
cli-table3@0.5.1, cli-table3@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
@@ -5345,6 +5438,14 @@ cli-truncate@2.1.0, cli-truncate@^2.1.0:
slice-ansi "^3.0.0"
string-width "^4.2.0"
cli-truncate@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574"
integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=
dependencies:
slice-ansi "0.0.4"
string-width "^1.0.1"
cli-width@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.1.tgz#b0433d0b4e9c847ef18868a4ef16fd5fc8271c48"
@@ -5524,6 +5625,11 @@ commander@^5.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
common-tags@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
integrity sha512-6P6g0uetGpW/sdyUy/iQQCbFF0kWVMSIVSyYz7Zgjcgh8mgw8PQzDNZeyZ5DQ2gM7LBoZPHmnjz8rUthkBG5tw==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -5572,7 +5678,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
concat-stream@^1.5.0:
concat-stream@^1.5.0, concat-stream@^1.6.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -6135,6 +6241,49 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@^4.12.0:
version "4.12.0"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.12.0.tgz#b9d9b16d45be28543edc0e4dc89987bed5bb090b"
integrity sha512-ZDngKMwoQ2UYmeSUJikLMZG6t2N7lTHHlzBzh5W0MbPfXSMv36YUgL2ZVD+t4ZLA63WWkvhwxIkDG+WJknBgHw==
dependencies:
"@cypress/listr-verbose-renderer" "^0.4.1"
"@cypress/request" "^2.88.5"
"@cypress/xvfb" "^1.2.4"
"@types/sinonjs__fake-timers" "^6.0.1"
"@types/sizzle" "^2.3.2"
arch "^2.1.2"
bluebird "^3.7.2"
cachedir "^2.3.0"
chalk "^2.4.2"
check-more-types "^2.24.0"
cli-table3 "~0.5.1"
commander "^4.1.1"
common-tags "^1.8.0"
debug "^4.1.1"
eventemitter2 "^6.4.2"
execa "^1.0.0"
executable "^4.1.1"
extract-zip "^1.7.0"
fs-extra "^8.1.0"
getos "^3.2.1"
is-ci "^2.0.0"
is-installed-globally "^0.3.2"
lazy-ass "^1.6.0"
listr "^0.14.3"
lodash "^4.17.19"
log-symbols "^3.0.0"
minimist "^1.2.5"
moment "^2.27.0"
ospath "^1.2.2"
pretty-bytes "^5.3.0"
ramda "~0.26.1"
request-progress "^3.0.0"
supports-color "^7.1.0"
tmp "~0.1.0"
untildify "^4.0.0"
url "^0.11.0"
yauzl "^2.10.0"
damerau-levenshtein@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@@ -6172,6 +6321,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^1.27.2:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"
integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==
date-fns@^2.4.1:
version "2.14.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba"
@@ -6714,6 +6868,11 @@ electron-to-chromium@^1.3.247, electron-to-chromium@^1.3.413:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.442.tgz#62f96e0529f40a214a97411b57f27c4f080f0aa2"
integrity sha512-3OjmbnD9+LyWzh9o3rjC7LNIkcDHjKyHM6Xt0G/+7gHGCaEIwvWYi8TrNA8feNnuGmvI9WKu289PFMQGMLHAig==
elegant-spinner@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
element-resize-detector@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.1.tgz#b0305194447a4863155e58f13323a0aef30851d1"
@@ -7092,6 +7251,13 @@ eslint-module-utils@^2.4.1:
debug "^2.6.9"
pkg-dir "^2.0.0"
eslint-plugin-cypress@^2.11.1:
version "2.11.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.11.1.tgz#a945e2774b88211e2c706a059d431e262b5c2862"
integrity sha512-MxMYoReSO5+IZMGgpBZHHSx64zYPSPTpXDwsgW7ChlJTF/sA+obqRbHplxD6sBStE+g4Mi0LCLkG4t9liu//mQ==
dependencies:
globals "^11.12.0"
eslint-plugin-flowtype@^4.3.0:
version "4.7.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-4.7.0.tgz#903a6ea3eb5cbf4c7ba7fa73cc43fc39ab7e4a70"
@@ -7292,6 +7458,11 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
eventemitter2@^6.4.2:
version "6.4.3"
resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820"
integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==
eventemitter3@^3.1.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
@@ -7370,6 +7541,18 @@ execa@^4.0.1:
signal-exit "^3.0.2"
strip-final-newline "^2.0.0"
executable@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c"
integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==
dependencies:
pify "^2.2.0"
exit-hook@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=
exit@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -7498,6 +7681,16 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
extract-zip@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.7.0.tgz#556cc3ae9df7f452c493a0cfb51cc30277940927"
integrity sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==
dependencies:
concat-stream "^1.6.2"
debug "^2.6.9"
mkdirp "^0.5.4"
yauzl "^2.10.0"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -7545,7 +7738,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fault@^1.0.0:
fault@^1.0.0, fault@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.4.tgz#eafcfc0a6d214fc94601e170df29954a4f842f13"
integrity sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==
@@ -7586,6 +7779,13 @@ fbjs@^0.8.1:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
fetch-mock@^7.5.1:
version "7.7.3"
resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.7.3.tgz#6a3f94cfed6e423ab7f5464912982030da605335"
@@ -7603,6 +7803,14 @@ figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
figures@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e"
integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=
dependencies:
escape-string-regexp "^1.0.5"
object-assign "^4.1.0"
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -8035,6 +8243,13 @@ get-value@^2.0.3, get-value@^2.0.6:
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=
getos@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5"
integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==
dependencies:
async "^3.2.0"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@@ -8091,7 +8306,7 @@ gitconfiglocal@^1.0.0:
dependencies:
ini "^1.3.2"
gitdiff-parser@^0.1.2, "gitdiff-parser@https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d":
gitdiff-parser@^0.1.2:
version "0.1.2"
resolved "https://github.com/scm-manager/gitdiff-parser#617747460280bf4522bb84d217a9064ac8eb6d3d"
@@ -8147,6 +8362,13 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, gl
once "^1.3.0"
path-is-absolute "^1.0.0"
global-dirs@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201"
integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==
dependencies:
ini "^1.3.5"
global-modules@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -8191,7 +8413,7 @@ global@^4.3.2, global@^4.4.0:
min-document "^2.19.0"
process "^0.11.10"
globals@^11.1.0:
globals@^11.1.0, globals@^11.12.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
@@ -9265,6 +9487,14 @@ is-hexadecimal@^1.0.0:
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
integrity sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==
is-installed-globally@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.2.tgz#fd3efa79ee670d1187233182d5b0a1dd00313141"
integrity sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==
dependencies:
global-dirs "^2.0.1"
is-path-inside "^3.0.1"
is-map@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1"
@@ -9302,6 +9532,13 @@ is-object@^1.0.1:
resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470"
integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA=
is-observable@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e"
integrity sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==
dependencies:
symbol-observable "^1.1.0"
is-path-cwd@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb"
@@ -9321,6 +9558,11 @@ is-path-inside@^2.1.0:
dependencies:
path-is-inside "^1.0.2"
is-path-inside@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017"
integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==
is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
@@ -9345,6 +9587,11 @@ is-potential-custom-element-name@^1.0.0:
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397"
integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c=
is-promise@^2.1.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
is-regex@^1.0.4, is-regex@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae"
@@ -10594,6 +10841,11 @@ last-call-webpack-plugin@^3.0.0:
lodash "^4.17.5"
webpack-sources "^1.1.0"
lazy-ass@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM=
lazy-cache@^0.2.3:
version "0.2.7"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65"
@@ -10705,6 +10957,35 @@ lint-staged@^10.2.11:
string-argv "0.3.1"
stringify-object "^3.3.0"
listr-silent-renderer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e"
integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=
listr-update-renderer@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2"
integrity sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==
dependencies:
chalk "^1.1.3"
cli-truncate "^0.2.1"
elegant-spinner "^1.0.1"
figures "^1.7.0"
indent-string "^3.0.0"
log-symbols "^1.0.2"
log-update "^2.3.0"
strip-ansi "^3.0.1"
listr-verbose-renderer@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db"
integrity sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==
dependencies:
chalk "^2.4.1"
cli-cursor "^2.1.0"
date-fns "^1.27.2"
figures "^2.0.0"
listr2@^2.1.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/listr2/-/listr2-2.4.1.tgz#006fc94ae77b3195403cbf3a4a563e2d6366224f"
@@ -10719,6 +11000,21 @@ listr2@^2.1.0:
rxjs "^6.6.0"
through "^2.3.8"
listr@^0.14.3:
version "0.14.3"
resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586"
integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==
dependencies:
"@samverschueren/stream-to-observable" "^0.3.0"
is-observable "^1.1.0"
is-promise "^2.1.0"
is-stream "^1.1.0"
listr-silent-renderer "^1.1.1"
listr-update-renderer "^0.5.0"
listr-verbose-renderer "^0.5.0"
p-map "^2.0.0"
rxjs "^6.3.3"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@@ -10881,6 +11177,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.set@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@@ -10921,6 +11222,25 @@ lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
lodash@^4.17.19:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
log-symbols@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=
dependencies:
chalk "^1.0.0"
log-symbols@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4"
integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==
dependencies:
chalk "^2.4.2"
log-symbols@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920"
@@ -10928,6 +11248,15 @@ log-symbols@^4.0.0:
dependencies:
chalk "^4.0.0"
log-update@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708"
integrity sha1-iDKP19HOeTiykoN0bwsbwSayRwg=
dependencies:
ansi-escapes "^3.0.0"
cli-cursor "^2.0.0"
wrap-ansi "^3.0.1"
log-update@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@@ -10965,7 +11294,7 @@ lower-case@^2.0.1:
dependencies:
tslib "^1.10.0"
lowlight@1.13.1, lowlight@^1.13.0, lowlight@~1.11.0:
lowlight@^1.13.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.13.1.tgz#c4f0e03906ebd23fedf2d258f6ab2f6324cf90eb"
integrity sha512-kQ71/T6RksEVz9AlPq07/2m+SU/1kGvt9k39UtvHX760u4SaWakaYH7hYgH5n6sTsCWk4MVYzUzLU59aN5CSmQ==
@@ -10973,6 +11302,14 @@ lowlight@1.13.1, lowlight@^1.13.0, lowlight@~1.11.0:
fault "^1.0.0"
highlight.js "~9.16.0"
lowlight@~1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
integrity sha512-xrGGN6XLL7MbTMdPD6NfWPwY43SNkjf/d0mecSx/CW36fUZTjRHEq0/Cdug3TWKtRXLWi7iMl1eP0olYxj/a4A==
dependencies:
fault "^1.0.2"
highlight.js "~9.13.0"
lru-cache@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
@@ -11468,7 +11805,7 @@ mkdirp@*:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1:
mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.1:
version "0.5.5"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
@@ -11480,6 +11817,11 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@^2.27.0:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
moo@^0.5.0:
version "0.5.1"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4"
@@ -12039,6 +12381,11 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
dependencies:
wrappy "1"
onetime@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789"
integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=
onetime@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4"
@@ -12159,6 +12506,11 @@ osenv@^0.1.4, osenv@^0.1.5:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
ospath@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b"
integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs=
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
@@ -12537,6 +12889,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -12547,7 +12904,7 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1:
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad"
integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==
pify@^2.0.0, pify@^2.3.0:
pify@^2.0.0, pify@^2.2.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@@ -13028,6 +13385,11 @@ prettier@^1.19.1:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
pretty-bytes@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.3.0.tgz#f2849e27db79fb4d6cfe24764fc4134f165989f2"
integrity sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==
pretty-error@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.1.tgz#5f4f87c8f91e5ae3f3ba87ab4cf5e03b1a17f1a3"
@@ -13351,7 +13713,7 @@ ramda@^0.21.0:
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.21.0.tgz#a001abedb3ff61077d4ff1d577d44de77e8d0a35"
integrity sha1-oAGr7bP/YQd9T/HVd9RN536NCjU=
ramda@^0.26:
ramda@^0.26, ramda@~0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
integrity sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ==
@@ -14161,6 +14523,13 @@ replace-ext@1.0.0:
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
request-progress@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe"
integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=
dependencies:
throttleit "^1.0.0"
request-promise-core@1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9"
@@ -14282,6 +14651,14 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.12.0, resolve@^1.13
dependencies:
path-parse "^1.0.6"
restore-cursor@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=
dependencies:
exit-hook "^1.0.0"
onetime "^1.0.0"
restore-cursor@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf"
@@ -14377,6 +14754,13 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
rxjs@^6.3.3, rxjs@^6.6.0:
version "6.6.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2"
integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==
dependencies:
tslib "^1.9.0"
rxjs@^6.4.0, rxjs@^6.5.3:
version "6.5.5"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.5.tgz#c5c884e3094c8cfee31bf27eb87e54ccfc87f9ec"
@@ -14384,13 +14768,6 @@ rxjs@^6.4.0, rxjs@^6.5.3:
dependencies:
tslib "^1.9.0"
rxjs@^6.6.0:
version "6.6.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2"
integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==
dependencies:
tslib "^1.9.0"
safe-buffer@5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
@@ -14773,6 +15150,11 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
slice-ansi@0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35"
integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=
slice-ansi@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636"
@@ -15484,7 +15866,7 @@ svgo@^1.0.0, svgo@^1.2.2:
unquote "~1.1.1"
util.promisify "~1.0.0"
symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.2.0:
symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
@@ -15676,6 +16058,11 @@ throttle-debounce@^2.1.0:
resolved "https://registry.yarnpkg.com/throttle-debounce/-/throttle-debounce-2.1.0.tgz#257e648f0a56bd9e54fe0f132c4ab8611df4e1d5"
integrity sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==
throttleit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=
through2@^2.0.0, through2@^2.0.2:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -15735,6 +16122,13 @@ tmp@^0.0.33:
dependencies:
os-tmpdir "~1.0.2"
tmp@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877"
integrity sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==
dependencies:
rimraf "^2.6.3"
tmpl@1.0.x:
version "1.0.4"
resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
@@ -16155,6 +16549,11 @@ unset-value@^1.0.0:
has-value "^0.3.1"
isobject "^3.0.0"
untildify@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
upath@^1.1.1, upath@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894"
@@ -16714,6 +17113,14 @@ worker-rpc@^0.1.0:
dependencies:
microevent.ts "~0.1.1"
wrap-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-3.0.1.tgz#288a04d87eda5c286e060dfe8f135ce8d007f8ba"
integrity sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo=
dependencies:
string-width "^2.1.1"
strip-ansi "^4.0.0"
wrap-ansi@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"
@@ -16965,3 +17372,11 @@ yargs@^15.3.1:
which-module "^2.0.0"
y18n "^4.0.0"
yargs-parser "^18.1.1"
yauzl@^2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"