mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
Merge with develop branch
This commit is contained in:
@@ -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 |
@@ -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 |
@@ -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.
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
33
scm-core/src/main/java/sonia/scm/security/AnonymousMode.java
Normal file
33
scm-core/src/main/java/sonia/scm/security/AnonymousMode.java
Normal 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
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import sonia.scm.SCMContext;
|
||||
|
||||
public class Authentications {
|
||||
|
||||
private Authentications() {}
|
||||
|
||||
public static boolean isAuthenticatedSubjectAnonymous() {
|
||||
return isSubjectAnonymous((String) SecurityUtils.getSubject().getPrincipal());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
3
scm-ui/e2e-tests/cypress.json
Normal file
3
scm-ui/e2e-tests/cypress.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:8081/scm"
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
44
scm-ui/e2e-tests/cypress/plugins/index.js
Normal file
44
scm-ui/e2e-tests/cypress/plugins/index.js
Normal 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
|
||||
}
|
||||
69
scm-ui/e2e-tests/cypress/support/commands.js
Normal file
69
scm-ui/e2e-tests/cypress/support/commands.js
Normal 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}]`));
|
||||
43
scm-ui/e2e-tests/cypress/support/index.js
Normal file
43
scm-ui/e2e-tests/cypress/support/index.js
Normal 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')
|
||||
26
scm-ui/e2e-tests/package.json
Normal file
26
scm-ui/e2e-tests/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
94
scm-ui/ui-components/src/devBuild.test.ts
Normal file
94
scm-ui/ui-components/src/devBuild.test.ts
Normal 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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
42
scm-ui/ui-components/src/devBuild.ts
Normal file
42
scm-ui/ui-components/src/devBuild.ts
Normal 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;
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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" />}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"repositories": "Repositories",
|
||||
"users": "Benutzer",
|
||||
"logout": "Abmelden",
|
||||
"login": "Anmelden",
|
||||
"groups": "Gruppen",
|
||||
"admin": "Administration"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
"repositories": "Repositories",
|
||||
"users": "Users",
|
||||
"logout": "Logout",
|
||||
"login": "Login",
|
||||
"groups": "Groups",
|
||||
"admin": "Administration"
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ class PermissionCheckbox extends React.Component<Props> {
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
testId={label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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`} />
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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" +
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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}&arch={arch}&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
465
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user