merge with 2.0.0-m3

This commit is contained in:
Sebastian Sdorra
2019-03-21 10:47:33 +01:00
91 changed files with 2102 additions and 1141 deletions

View File

@@ -174,8 +174,8 @@ public class ScmConfiguration implements Configuration {
@XmlElement(name = "xsrf-protection") @XmlElement(name = "xsrf-protection")
private boolean enabledXsrfProtection = true; private boolean enabledXsrfProtection = true;
@XmlElement(name = "default-namespace-strategy") @XmlElement(name = "namespace-strategy")
private String defaultNamespaceStrategy = "sonia.scm.repository.DefaultNamespaceStrategy"; private String namespaceStrategy = "UsernameNamespaceStrategy";
/** /**
@@ -215,7 +215,7 @@ public class ScmConfiguration implements Configuration {
this.loginAttemptLimit = other.loginAttemptLimit; this.loginAttemptLimit = other.loginAttemptLimit;
this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout; this.loginAttemptLimitTimeout = other.loginAttemptLimitTimeout;
this.enabledXsrfProtection = other.enabledXsrfProtection; this.enabledXsrfProtection = other.enabledXsrfProtection;
this.defaultNamespaceStrategy = other.defaultNamespaceStrategy; this.namespaceStrategy = other.namespaceStrategy;
} }
/** /**
@@ -346,8 +346,8 @@ public class ScmConfiguration implements Configuration {
return loginAttemptLimit > 0; return loginAttemptLimit > 0;
} }
public String getDefaultNamespaceStrategy() { public String getNamespaceStrategy() {
return defaultNamespaceStrategy; return namespaceStrategy;
} }
@@ -473,8 +473,8 @@ public class ScmConfiguration implements Configuration {
this.enabledXsrfProtection = enabledXsrfProtection; this.enabledXsrfProtection = enabledXsrfProtection;
} }
public void setDefaultNamespaceStrategy(String defaultNamespaceStrategy) { public void setNamespaceStrategy(String namespaceStrategy) {
this.defaultNamespaceStrategy = defaultNamespaceStrategy; this.namespaceStrategy = namespaceStrategy;
} }
@Override @Override

View File

@@ -0,0 +1,22 @@
package sonia.scm.group;
import java.util.Collection;
/**
* This class represents all associated groups which are provided by external systems for a certain user.
*
* @author Sebastian Sdorra
* @since 2.0.0
*/
public class ExternalGroupNames extends GroupNames {
public ExternalGroupNames() {
}
public ExternalGroupNames(String groupName, String... groupNames) {
super(groupName, groupNames);
}
public ExternalGroupNames(Collection<String> collection) {
super(collection);
}
}

View File

@@ -52,7 +52,7 @@ import java.util.Iterator;
* @author Sebastian Sdorra * @author Sebastian Sdorra
* @since 1.21 * @since 1.21
*/ */
public final class GroupNames implements Serializable, Iterable<String> public class GroupNames implements Serializable, Iterable<String>
{ {
/** /**
@@ -94,20 +94,8 @@ public final class GroupNames implements Serializable, Iterable<String>
* @param collection * @param collection
*/ */
public GroupNames(Collection<String> collection) public GroupNames(Collection<String> collection)
{
this(collection, false);
}
/**
* Constructs ...
*
*
* @param collection
*/
public GroupNames(Collection<String> collection, boolean external)
{ {
this.collection = Collections.unmodifiableCollection(collection); this.collection = Collections.unmodifiableCollection(collection);
this.external = external;
} }
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
@@ -176,7 +164,7 @@ public final class GroupNames implements Serializable, Iterable<String>
@Override @Override
public String toString() public String toString()
{ {
return Joiner.on(", ").join(collection) + "(" + (external? "external": "internal") + ")"; return Joiner.on(", ").join(collection);
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -192,13 +180,8 @@ public final class GroupNames implements Serializable, Iterable<String>
return collection; return collection;
} }
public boolean isExternal() {
return external;
}
//~--- fields --------------------------------------------------------------- //~--- fields ---------------------------------------------------------------
/** Field description */ /** Field description */
private final Collection<String> collection; private final Collection<String> collection;
private final boolean external;
} }

View File

@@ -248,7 +248,8 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
/** /**
* Returns true if the {@link Repository} is valid. * Returns true if the {@link Repository} is valid.
* <ul> * <ul>
* <li>The name is not empty and contains only A-z, 0-9, _, -, /</li> * <li>The namespace is valid</li>
* <li>The name is valid</li>
* <li>The type is not empty</li> * <li>The type is not empty</li>
* <li>The contact is empty or contains a valid email address</li> * <li>The contact is empty or contains a valid email address</li>
* </ul> * </ul>
@@ -257,9 +258,10 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
*/ */
@Override @Override
public boolean isValid() { public boolean isValid() {
return ValidationUtil.isRepositoryNameValid(name) && Util.isNotEmpty(type) return ValidationUtil.isRepositoryNameValid(namespace)
&& ((Util.isEmpty(contact)) && ValidationUtil.isRepositoryNameValid(name)
|| ValidationUtil.isMailAddressValid(contact)); && Util.isNotEmpty(type)
&& ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
} }
/** /**

View File

@@ -35,8 +35,6 @@ package sonia.scm.security;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSet.Builder;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.DisabledAccountException; import org.apache.shiro.authc.DisabledAccountException;
@@ -47,9 +45,7 @@ import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.subject.SimplePrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserDAO; import sonia.scm.user.UserDAO;
@@ -75,7 +71,7 @@ public final class DAORealmHelper {
private final UserDAO userDAO; private final UserDAO userDAO;
private final GroupDAO groupDAO; private final GroupCollector groupCollector;
private final String realm; private final String realm;
@@ -87,14 +83,14 @@ public final class DAORealmHelper {
* *
* @param loginAttemptHandler login attempt handler for wrapping credentials matcher * @param loginAttemptHandler login attempt handler for wrapping credentials matcher
* @param userDAO user dao * @param userDAO user dao
* @param groupDAO group dao * @param groupCollector collect groups for a principal
* @param realm name of realm * @param realm name of realm
*/ */
public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO, String realm) { public DAORealmHelper(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupCollector groupCollector, String realm) {
this.loginAttemptHandler = loginAttemptHandler; this.loginAttemptHandler = loginAttemptHandler;
this.realm = realm; this.realm = realm;
this.userDAO = userDAO; this.userDAO = userDAO;
this.groupDAO = groupDAO; this.groupCollector = groupCollector;
} }
//~--- get methods ---------------------------------------------------------- //~--- get methods ----------------------------------------------------------
@@ -157,7 +153,7 @@ public final class DAORealmHelper {
collection.add(principal, realm); collection.add(principal, realm);
collection.add(user, realm); collection.add(user, realm);
collection.add(collectGroups(principal, groups), realm); collection.add(groupCollector.collect(principal, groups), realm);
collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm); collection.add(MoreObjects.firstNonNull(scope, Scope.empty()), realm);
String creds = credentials; String creds = credentials;
@@ -171,26 +167,6 @@ public final class DAORealmHelper {
//~--- methods -------------------------------------------------------------- //~--- methods --------------------------------------------------------------
private GroupNames collectGroups(String principal, Iterable<String> groupNames) {
Builder<String> builder = ImmutableSet.builder();
builder.add(GroupNames.AUTHENTICATED);
for (String group : groupNames) {
builder.add(group);
}
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
}
}
GroupNames groups = new GroupNames(builder.build());
LOG.debug("collected following groups for principal {}: {}", principal, groups);
return groups;
}
/** /**
* Builder class for {@link AuthenticationInfo}. * Builder class for {@link AuthenticationInfo}.
*/ */

View File

@@ -30,10 +30,11 @@
*/ */
package sonia.scm.security; package sonia.scm.security;
import javax.inject.Inject;
import sonia.scm.group.GroupDAO; import sonia.scm.group.GroupDAO;
import sonia.scm.user.UserDAO; import sonia.scm.user.UserDAO;
import javax.inject.Inject;
/** /**
* Factory to create {@link DAORealmHelper} instances. * Factory to create {@link DAORealmHelper} instances.
* *
@@ -44,7 +45,7 @@ public final class DAORealmHelperFactory {
private final LoginAttemptHandler loginAttemptHandler; private final LoginAttemptHandler loginAttemptHandler;
private final UserDAO userDAO; private final UserDAO userDAO;
private final GroupDAO groupDAO; private final GroupCollector groupCollector;
/** /**
* Constructs a new instance. * Constructs a new instance.
@@ -57,7 +58,7 @@ public final class DAORealmHelperFactory {
public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) { public DAORealmHelperFactory(LoginAttemptHandler loginAttemptHandler, UserDAO userDAO, GroupDAO groupDAO) {
this.loginAttemptHandler = loginAttemptHandler; this.loginAttemptHandler = loginAttemptHandler;
this.userDAO = userDAO; this.userDAO = userDAO;
this.groupDAO = groupDAO; this.groupCollector = new GroupCollector(groupDAO);
} }
/** /**
@@ -68,7 +69,7 @@ public final class DAORealmHelperFactory {
* @return new {@link DAORealmHelper} instance. * @return new {@link DAORealmHelper} instance.
*/ */
public DAORealmHelper create(String realm) { public DAORealmHelper create(String realm) {
return new DAORealmHelper(loginAttemptHandler, userDAO, groupDAO, realm); return new DAORealmHelper(loginAttemptHandler, userDAO, groupCollector, realm);
} }
} }

View File

@@ -0,0 +1,43 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
/**
* Collect groups for a certain principal.
* <strong>Warning</strong>: The class is only for internal use and should never used directly.
*/
class GroupCollector {
private static final Logger LOG = LoggerFactory.getLogger(GroupCollector.class);
private final GroupDAO groupDAO;
GroupCollector(GroupDAO groupDAO) {
this.groupDAO = groupDAO;
}
GroupNames collect(String principal, Iterable<String> groupNames) {
ImmutableSet.Builder<String> builder = ImmutableSet.builder();
builder.add(GroupNames.AUTHENTICATED);
for (String group : groupNames) {
builder.add(group);
}
for (Group group : groupDAO.getAll()) {
if (group.isMember(principal)) {
builder.add(group.getName());
}
}
GroupNames groups = new GroupNames(builder.build());
LOG.debug("collected following groups for principal {}: {}", principal, groups);
return groups;
}
}

View File

@@ -28,7 +28,6 @@
*/ */
package sonia.scm.security; package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import com.google.inject.Inject; import com.google.inject.Inject;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.SimpleAuthenticationInfo;
@@ -37,20 +36,19 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.NotFoundException; import sonia.scm.NotFoundException;
import sonia.scm.group.ExternalGroupNames;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames;
import sonia.scm.plugin.Extension; import sonia.scm.plugin.Extension;
import sonia.scm.user.User; import sonia.scm.user.User;
import sonia.scm.user.UserManager; import sonia.scm.user.UserManager;
import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.AdministrationContext;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
/** /**
* Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated * Helper class for syncing realms. The class should simplify the creation of realms, which are syncing authenticated
@@ -65,24 +63,24 @@ public final class SyncingRealmHelper {
private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class); private static final Logger LOG = LoggerFactory.getLogger(SyncingRealmHelper.class);
private final AdministrationContext ctx; private final AdministrationContext ctx;
private final GroupManager groupManager;
private final UserManager userManager; private final UserManager userManager;
private final GroupManager groupManager;
private final GroupCollector groupCollector;
/** /**
* Constructs a new SyncingRealmHelper. * Constructs a new SyncingRealmHelper.
* *
*
* @param ctx administration context * @param ctx administration context
* @param userManager user manager * @param userManager user manager
* @param groupManager group manager * @param groupManager group manager
* @param groupDAO group dao
*/ */
@Inject @Inject
public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager) { public SyncingRealmHelper(AdministrationContext ctx, UserManager userManager, GroupManager groupManager, GroupDAO groupDAO) {
this.ctx = ctx; this.ctx = ctx;
this.userManager = userManager; this.userManager = userManager;
this.groupManager = groupManager; this.groupManager = groupManager;
this.groupCollector = new GroupCollector(groupDAO);
} }
/** /**
@@ -95,11 +93,11 @@ public final class SyncingRealmHelper {
public class AuthenticationInfoBuilder { public class AuthenticationInfoBuilder {
private String realm; private String realm;
private User user; private User user;
private Collection<String> groups; private Collection<String> groups = Collections.emptySet();
private boolean external; private Collection<String> externalGroups = Collections.emptySet();
private AuthenticationInfo build() { private AuthenticationInfo build() {
return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, external); return SyncingRealmHelper.this.createAuthenticationInfo(realm, user, groups, externalGroups);
} }
public class ForRealm { public class ForRealm {
@@ -134,52 +132,51 @@ public final class SyncingRealmHelper {
private WithGroups() { private WithGroups() {
} }
/**
* Build the authentication info without groups.
* @return The complete {@link AuthenticationInfo}
*/
public AuthenticationInfo withoutGroups() {
return withGroups(emptyList());
}
/** /**
* Set the internal groups for the user. * Set the internal groups for the user.
* @param groups groups of the authenticated user * @param groups groups of the authenticated user
* @return The complete {@link AuthenticationInfo} * @return builder step for groups
*/ */
public AuthenticationInfo withGroups(String... groups) { public WithGroups withGroups(String... groups) {
return withGroups(asList(groups)); return withGroups(asList(groups));
} }
/** /**
* Set the internal groups for the user. * Set the internal groups for the user.
* @param groups groups of the authenticated user * @param groups groups of the authenticated user
* @return The complete {@link AuthenticationInfo} * @return builder step for groups
*/ */
public AuthenticationInfo withGroups(Collection<String> groups) { public WithGroups withGroups(Collection<String> groups) {
AuthenticationInfoBuilder.this.groups = groups; AuthenticationInfoBuilder.this.groups = groups;
AuthenticationInfoBuilder.this.external = false; return this;
return build();
} }
/** /**
* Set the external groups for the user. * Set the external groups for the user.
* @param groups external groups of the authenticated user * @param externalGroups external groups of the authenticated user
* @return The complete {@link AuthenticationInfo} * @return builder step for groups
*/ */
public AuthenticationInfo withExternalGroups(String... groups) { public WithGroups withExternalGroups(String... externalGroups) {
return withExternalGroups(asList(groups)); return withExternalGroups(asList(externalGroups));
} }
/** /**
* Set the external groups for the user. * Set the external groups for the user.
* @param groups external groups of the authenticated user * @param externalGroups external groups of the authenticated user
* @return The complete {@link AuthenticationInfo} * @return builder step for groups
*/ */
public AuthenticationInfo withExternalGroups(Collection<String> groups) { public WithGroups withExternalGroups(Collection<String> externalGroups) {
AuthenticationInfoBuilder.this.groups = groups; AuthenticationInfoBuilder.this.externalGroups = externalGroups;
AuthenticationInfoBuilder.this.external = true; return this;
return build(); }
/**
* Builds the {@link AuthenticationInfo} from the given options.
*
* @return complete autentication info
*/
public AuthenticationInfo build() {
return AuthenticationInfoBuilder.this.build();
} }
} }
} }
@@ -197,12 +194,13 @@ public final class SyncingRealmHelper {
* @return authentication info * @return authentication info
*/ */
private AuthenticationInfo createAuthenticationInfo(String realm, User user, private AuthenticationInfo createAuthenticationInfo(String realm, User user,
Collection<String> groups, boolean externalGroups) { Collection<String> groups, Collection<String> externalGroups) {
SimplePrincipalCollection collection = new SimplePrincipalCollection(); SimplePrincipalCollection collection = new SimplePrincipalCollection();
collection.add(user.getId(), realm); collection.add(user.getId(), realm);
collection.add(user, realm); collection.add(user, realm);
collection.add(new GroupNames(groups, externalGroups), realm); collection.add(groupCollector.collect(user.getId(), groups), realm);
collection.add(new ExternalGroupNames(externalGroups), realm);
return new SimpleAuthenticationInfo(collection, user.getPassword()); return new SimpleAuthenticationInfo(collection, user.getPassword());
} }

View File

@@ -35,14 +35,12 @@ package sonia.scm.util;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Splitter;
import sonia.scm.Validateable; import sonia.scm.Validateable;
//~--- JDK imports ------------------------------------------------------------
import java.util.regex.Pattern; import java.util.regex.Pattern;
//~--- JDK imports ------------------------------------------------------------
/** /**
* *
* @author Sebastian Sdorra * @author Sebastian Sdorra
@@ -52,15 +50,16 @@ public final class ValidationUtil
/** Field description */ /** Field description */
private static final String REGEX_MAIL = private static final String REGEX_MAIL =
"^[A-z0-9][\\w.-]*@[A-z0-9][\\w\\-\\.]*\\.[A-z0-9][A-z0-9-]+$"; "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$";
/** Field description */ /** Field description */
private static final String REGEX_NAME = private static final String REGEX_NAME =
"^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$"; "^[A-Za-z0-9\\.\\-_@]|[^ ]([A-Za-z0-9\\.\\-_@ ]*[A-Za-z0-9\\.\\-_@]|[^ ])?$";
public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$";
/** Field description */ /** Field description */
private static final String REGEX_REPOSITORYNAME = private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);
"(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$";
//~--- constructors --------------------------------------------------------- //~--- constructors ---------------------------------------------------------
@@ -142,37 +141,15 @@ public final class ValidationUtil
} }
/** /**
* Method description * Returns {@code true} if the repository name is valid.
* *
* * @param name repository name
* @param name
* @since 1.9 * @since 1.9
* *
* @return * @return {@code true} if repository name is valid
*/ */
public static boolean isRepositoryNameValid(String name) public static boolean isRepositoryNameValid(String name) {
{ return PATTERN_REPOSITORYNAME.matcher(name).matches();
Pattern pattern = Pattern.compile(REGEX_REPOSITORYNAME);
boolean result = true;
if (Util.isNotEmpty(name))
{
for (String p : Splitter.on('/').split(name))
{
if (!pattern.matcher(p).matches())
{
result = false;
break;
}
}
}
else
{
result = false;
}
return result;
} }
/** /**

View File

@@ -46,6 +46,8 @@ public class VndMediaType {
public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX; public static final String MERGE_RESULT = PREFIX + "mergeResult" + SUFFIX;
public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX; public static final String MERGE_COMMAND = PREFIX + "mergeCommand" + SUFFIX;
public static final String NAMESPACE_STRATEGIES = PREFIX + "namespaceStrategies" + SUFFIX;
public static final String ME = PREFIX + "me" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX;
public static final String SOURCE = PREFIX + "source" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX;
public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX; public static final String ERROR_TYPE = PREFIX + "error" + SUFFIX;

View File

@@ -6,6 +6,7 @@ import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.PrincipalCollection;
import org.junit.Ignore;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -37,7 +38,7 @@ class DAORealmHelperTest {
@BeforeEach @BeforeEach
void setUpObjectUnderTest() { void setUpObjectUnderTest() {
helper = new DAORealmHelper(loginAttemptHandler, userDAO, groupDAO, "hitchhiker"); helper = new DAORealmHelper(loginAttemptHandler, userDAO, new GroupCollector(groupDAO), "hitchhiker");
} }
@Test @Test
@@ -77,6 +78,7 @@ class DAORealmHelperTest {
} }
@Test @Test
@Ignore
void shouldReturnAuthenticationInfoWithGroups() { void shouldReturnAuthenticationInfoWithGroups() {
User user = new User("trillian"); User user = new User("trillian");
when(userDAO.get("trillian")).thenReturn(user); when(userDAO.get("trillian")).thenReturn(user);

View File

@@ -0,0 +1,64 @@
package sonia.scm.security;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupNames;
import java.util.Collections;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GroupCollectorTest {
@Mock
private GroupDAO groupDAO;
@InjectMocks
private GroupCollector collector;
@Test
void shouldAlwaysReturnAuthenticatedGroup() {
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
assertThat(groupNames).containsOnly("_authenticated");
}
@Nested
class WithGroupsFromDao {
@BeforeEach
void setUpGroupsDao() {
List<Group> groups = Lists.newArrayList(
new Group("xml", "heartOfGold", "trillian"),
new Group("xml", "g42", "dent", "prefect"),
new Group("xml", "fjordsOfAfrican", "dent", "trillian")
);
when(groupDAO.getAll()).thenReturn(groups);
}
@Test
void shouldReturnGroupsFromDao() {
GroupNames groupNames = collector.collect("trillian", Collections.emptySet());
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
}
@Test
void shouldCombineGivenWithDao() {
GroupNames groupNames = collector.collect("trillian", ImmutableList.of("awesome", "incredible"));
assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican", "awesome", "incredible");
}
}
}

View File

@@ -36,6 +36,7 @@ package sonia.scm.security;
//~--- non-JDK imports -------------------------------------------------------- //~--- non-JDK imports --------------------------------------------------------
import com.google.common.base.Throwables; import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.Before; import org.junit.Before;
@@ -44,7 +45,9 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.AlreadyExistsException; import sonia.scm.AlreadyExistsException;
import sonia.scm.group.ExternalGroupNames;
import sonia.scm.group.Group; import sonia.scm.group.Group;
import sonia.scm.group.GroupDAO;
import sonia.scm.group.GroupManager; import sonia.scm.group.GroupManager;
import sonia.scm.group.GroupNames; import sonia.scm.group.GroupNames;
import sonia.scm.user.User; import sonia.scm.user.User;
@@ -53,19 +56,11 @@ import sonia.scm.web.security.AdministrationContext;
import sonia.scm.web.security.PrivilegedAction; import sonia.scm.web.security.PrivilegedAction;
import java.io.IOException; import java.io.IOException;
import java.util.List;
import static java.util.Collections.singletonList;
import static org.assertj.core.util.Arrays.asList;
import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasItem;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse; import static org.mockito.Mockito.*;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
//~--- JDK imports ------------------------------------------------------------ //~--- JDK imports ------------------------------------------------------------
@@ -83,6 +78,9 @@ public class SyncingRealmHelperTest {
@Mock @Mock
private UserManager userManager; private UserManager userManager;
@Mock
private GroupDAO groupDAO;
private SyncingRealmHelper helper; private SyncingRealmHelper helper;
/** /**
@@ -108,7 +106,7 @@ public class SyncingRealmHelperTest {
} }
}; };
helper = new SyncingRealmHelper(ctx, userManager, groupManager); helper = new SyncingRealmHelper(ctx, userManager, groupManager, groupDAO);
} }
/** /**
@@ -191,11 +189,11 @@ public class SyncingRealmHelperTest {
.authenticationInfo() .authenticationInfo()
.forRealm("unit-test") .forRealm("unit-test")
.andUser(new User("ziltoid")) .andUser(new User("ziltoid"))
.withGroups("internal"); .withGroups("internal")
.build();
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class); GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames.getCollection()).containsOnly("internal"); Assertions.assertThat(groupNames.getCollection()).contains("_authenticated", "internal");
Assertions.assertThat(groupNames.isExternal()).isFalse();
} }
@Test @Test
@@ -204,11 +202,11 @@ public class SyncingRealmHelperTest {
.authenticationInfo() .authenticationInfo()
.forRealm("unit-test") .forRealm("unit-test")
.andUser(new User("ziltoid")) .andUser(new User("ziltoid"))
.withExternalGroups("external"); .withExternalGroups("external")
.build();
GroupNames groupNames = authenticationInfo.getPrincipals().oneByType(GroupNames.class); ExternalGroupNames groupNames = authenticationInfo.getPrincipals().oneByType(ExternalGroupNames.class);
Assertions.assertThat(groupNames.getCollection()).containsOnly("external"); Assertions.assertThat(groupNames.getCollection()).containsOnly("external");
Assertions.assertThat(groupNames.isExternal()).isTrue();
} }
@Test @Test
@@ -218,11 +216,34 @@ public class SyncingRealmHelperTest {
.authenticationInfo() .authenticationInfo()
.forRealm("unit-test") .forRealm("unit-test")
.andUser(user) .andUser(user)
.withoutGroups(); .build();
assertNotNull(authInfo); assertNotNull(authInfo);
assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal()); assertEquals("ziltoid", authInfo.getPrincipals().getPrimaryPrincipal());
assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test")); assertThat(authInfo.getPrincipals().getRealmNames(), hasItem("unit-test"));
assertEquals(user, authInfo.getPrincipals().oneByType(User.class)); assertEquals(user, authInfo.getPrincipals().oneByType(User.class));
} }
@Test
public void shouldReturnCombinedGroupNames() {
User user = new User("tricia");
List<Group> groups = Lists.newArrayList(new Group("xml", "heartOfGold", "tricia"));
when(groupDAO.getAll()).thenReturn(groups);
AuthenticationInfo authInfo = helper
.authenticationInfo()
.forRealm("unit-test")
.andUser(user)
.withGroups("fjordsOfAfrican")
.withExternalGroups("g42")
.build();
GroupNames groupNames = authInfo.getPrincipals().oneByType(GroupNames.class);
Assertions.assertThat(groupNames).contains("_authenticated", "heartOfGold", "fjordsOfAfrican");
ExternalGroupNames externalGroupNames = authInfo.getPrincipals().oneByType(ExternalGroupNames.class);
Assertions.assertThat(externalGroupNames).contains("g42");
}
} }

View File

@@ -143,51 +143,21 @@ public class ValidationUtilTest
assertFalse(ValidationUtil.isNotContaining("test", "t")); assertFalse(ValidationUtil.isNotContaining("test", "t"));
} }
/**
* Method description
*
*/
@Test @Test
public void testIsRepositoryNameValid() public void testIsRepositoryNameValid() {
{
assertTrue(ValidationUtil.isRepositoryNameValid("scm"));
assertTrue(ValidationUtil.isRepositoryNameValid("scm/main"));
assertTrue(ValidationUtil.isRepositoryNameValid("scm/plugins/git-plugin"));
assertTrue(ValidationUtil.isRepositoryNameValid("s"));
assertTrue(ValidationUtil.isRepositoryNameValid("sc"));
assertTrue(ValidationUtil.isRepositoryNameValid(".scm/plugins"));
// issue 142
assertFalse(ValidationUtil.isRepositoryNameValid("."));
assertFalse(ValidationUtil.isRepositoryNameValid("/"));
assertFalse(ValidationUtil.isRepositoryNameValid("scm/plugins/."));
assertFalse(ValidationUtil.isRepositoryNameValid("scm/../plugins"));
assertFalse(ValidationUtil.isRepositoryNameValid("scm/main/"));
assertFalse(ValidationUtil.isRepositoryNameValid("/scm/main/"));
// issue 144
assertFalse(ValidationUtil.isRepositoryNameValid("scm/./main"));
assertFalse(ValidationUtil.isRepositoryNameValid("scm//main"));
// issue 148
//J-
String[] validPaths = { String[] validPaths = {
"scm", "scm",
"scm/main",
"scm/plugins/git-plugin",
"s", "s",
"sc", "sc",
".scm/plugins",
".hiddenrepo", ".hiddenrepo",
"b.", "b.",
"...", "...",
"..c", "..c",
"d..", "d..",
"a/b..", "a..c"
"a/..b",
"a..c",
}; };
// issue 142, 144 and 148
String[] invalidPaths = { String[] invalidPaths = {
".", ".",
"/", "/",
@@ -228,17 +198,22 @@ public class ValidationUtilTest
"abc)abc", "abc)abc",
"abc[abc", "abc[abc",
"abc]abc", "abc]abc",
"abc|abc" "abc|abc",
"scm/main",
"scm/plugins/git-plugin",
".scm/plugins",
"a/b..",
"a/..b",
"scm/main",
"scm/plugins/git-plugin",
"scm/plugins/git-plugin"
}; };
//J+
for (String path : validPaths) for (String path : validPaths) {
{
assertTrue(ValidationUtil.isRepositoryNameValid(path)); assertTrue(ValidationUtil.isRepositoryNameValid(path));
} }
for (String path : invalidPaths) for (String path : invalidPaths) {
{
assertFalse(ValidationUtil.isRepositoryNameValid(path)); assertFalse(ValidationUtil.isRepositoryNameValid(path));
} }
} }

View File

@@ -8,6 +8,7 @@ import sonia.scm.repository.GitRepositoryConfig;
import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.NamespaceAndName;
import sonia.scm.repository.Repository; import sonia.scm.repository.Repository;
import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryManager;
import sonia.scm.repository.RepositoryPermissions;
import sonia.scm.store.ConfigurationStore; import sonia.scm.store.ConfigurationStore;
import sonia.scm.web.GitVndMediaType; import sonia.scm.web.GitVndMediaType;
@@ -50,6 +51,7 @@ public class GitRepositoryConfigResource {
}) })
public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) { public Response getRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name) {
Repository repository = getRepository(namespace, name); Repository repository = getRepository(namespace, name);
RepositoryPermissions.read(repository).check();
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository); ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
GitRepositoryConfig config = repositoryConfigStore.get(); GitRepositoryConfig config = repositoryConfigStore.get();
GitRepositoryConfigDto dto = repositoryConfigMapper.map(config, repository); GitRepositoryConfigDto dto = repositoryConfigMapper.map(config, repository);
@@ -68,6 +70,7 @@ public class GitRepositoryConfigResource {
}) })
public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) { public Response setRepositoryConfig(@PathParam("namespace") String namespace, @PathParam("name") String name, GitRepositoryConfigDto dto) {
Repository repository = getRepository(namespace, name); Repository repository = getRepository(namespace, name);
RepositoryPermissions.modify(repository).check();
ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository); ConfigurationStore<GitRepositoryConfig> repositoryConfigStore = getStore(repository);
GitRepositoryConfig config = repositoryConfigMapper.map(dto); GitRepositoryConfig config = repositoryConfigMapper.map(dto);
repositoryConfigStore.set(config); repositoryConfigStore.set(config);

View File

@@ -9,7 +9,6 @@ type Configuration = {
repositoryDirectory?: string, repositoryDirectory?: string,
gcExpression?: string, gcExpression?: string,
nonFastForwardDisallowed: boolean, nonFastForwardDisallowed: boolean,
disabled: boolean,
_links: Links _links: Links
} }
@@ -42,7 +41,7 @@ class GitConfigurationForm extends React.Component<Props, State> {
}; };
render() { render() {
const { gcExpression, nonFastForwardDisallowed, disabled } = this.state; const { gcExpression, nonFastForwardDisallowed } = this.state;
const { readOnly, t } = this.props; const { readOnly, t } = this.props;
return ( return (
@@ -61,13 +60,6 @@ class GitConfigurationForm extends React.Component<Props, State> {
onChange={this.handleChange} onChange={this.handleChange}
disabled={readOnly} disabled={readOnly}
/> />
<Checkbox name="disabled"
label={t("scm-git-plugin.config.disabled")}
helpText={t("scm-git-plugin.config.disabledHelpText")}
checked={disabled}
onChange={this.handleChange}
disabled={readOnly}
/>
</> </>
); );
} }

View File

@@ -18,7 +18,8 @@ type State = {
error?: Error, error?: Error,
branches: Branch[], branches: Branch[],
selectedBranchName?: string, selectedBranchName?: string,
defaultBranchChanged: boolean defaultBranchChanged: boolean,
disabled: boolean
}; };
const GIT_CONFIG_CONTENT_TYPE = "application/vnd.scmm-gitConfig+json"; const GIT_CONFIG_CONTENT_TYPE = "application/vnd.scmm-gitConfig+json";
@@ -33,7 +34,8 @@ class RepositoryConfig extends React.Component<Props, State> {
loadingDefaultBranch: true, loadingDefaultBranch: true,
submitPending: false, submitPending: false,
branches: [], branches: [],
defaultBranchChanged: false defaultBranchChanged: false,
disabled: true
}; };
} }
@@ -53,11 +55,11 @@ class RepositoryConfig extends React.Component<Props, State> {
apiClient apiClient
.get(repository._links.configuration.href) .get(repository._links.configuration.href)
.then(response => response.json()) .then(response => response.json())
.then(payload => payload.defaultBranch) .then(payload =>
.then(selectedBranchName =>
this.setState({ this.setState({
...this.state, ...this.state,
selectedBranchName, selectedBranchName: payload.defaultBranch,
disabled: !payload._links.update,
loadingDefaultBranch: false loadingDefaultBranch: false
}) })
) )
@@ -98,7 +100,7 @@ class RepositoryConfig extends React.Component<Props, State> {
render() { render() {
const { t } = this.props; const { t } = this.props;
const { loadingBranches, loadingDefaultBranch, submitPending, error } = this.state; const { loadingBranches, loadingDefaultBranch, submitPending, error, disabled } = this.state;
if (error) { if (error) {
return ( return (
@@ -110,6 +112,12 @@ class RepositoryConfig extends React.Component<Props, State> {
); );
} }
const submitButton = disabled? null: <SubmitButton
label={t("scm-git-plugin.repo-config.submit")}
loading={submitPending}
disabled={!this.state.selectedBranchName}
/>;
if (!(loadingBranches || loadingDefaultBranch)) { if (!(loadingBranches || loadingDefaultBranch)) {
return ( return (
<> <>
@@ -121,12 +129,9 @@ class RepositoryConfig extends React.Component<Props, State> {
branches={this.state.branches} branches={this.state.branches}
selected={this.branchSelected} selected={this.branchSelected}
selectedBranch={this.state.selectedBranchName} selectedBranch={this.state.selectedBranchName}
disabled={disabled}
/> />
<SubmitButton { submitButton }
label={t("scm-git-plugin.repo-config.submit")}
loading={submitPending}
disabled={!this.state.selectedBranchName}
/>
</form> </form>
<hr /> <hr />
</> </>

View File

@@ -159,7 +159,7 @@ public class GitConfigResourceTest {
} }
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "readWrite")
public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { public void shouldReadDefaultRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
@@ -193,7 +193,7 @@ public class GitConfigResourceTest {
} }
@Test @Test
@SubjectAware(username = "writeOnly") @SubjectAware(username = "readOnly")
public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException { public void shouldReadStoredRepositoryConfig() throws URISyntaxException, UnsupportedEncodingException {
when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X")); when(repositoryManager.get(new NamespaceAndName("space", "X"))).thenReturn(new Repository("id", "git", "space", "X"));
GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig(); GitRepositoryConfig gitRepositoryConfig = new GitRepositoryConfig();

View File

@@ -7,7 +7,7 @@ admin = secret, admin
[roles] [roles]
reader = configuration:read:git reader = configuration:read:git
writer = configuration:write:git writer = configuration:write:git
readerWriter = configuration:*:git readerWriter = configuration:*:git,repository:*:id
admin = * admin = *
repoRead = repository:read:* repoRead = repository:read:*
repoWrite = repository:modify:* repoWrite = repository:modify:*

View File

@@ -13,7 +13,6 @@ type Configuration = {
"showRevisionInId": boolean, "showRevisionInId": boolean,
"disableHookSSLValidation": boolean, "disableHookSSLValidation": boolean,
"enableHttpPostArgs": boolean, "enableHttpPostArgs": boolean,
"disabled": boolean,
"_links": Links "_links": Links
}; };
@@ -105,7 +104,6 @@ class HgConfigurationForm extends React.Component<Props, State> {
{this.checkbox("showRevisionInId")} {this.checkbox("showRevisionInId")}
{this.checkbox("disableHookSSLValidation")} {this.checkbox("disableHookSSLValidation")}
{this.checkbox("enableHttpPostArgs")} {this.checkbox("enableHttpPostArgs")}
{this.checkbox("disabled")}
</> </>
); );
} }

View File

@@ -7,7 +7,6 @@ import { InputField, Checkbox, Select } from "@scm-manager/ui-components";
type Configuration = { type Configuration = {
compatibility: string, compatibility: string,
enabledGZip: boolean, enabledGZip: boolean,
disabled: boolean,
_links: Links _links: Links
}; };
@@ -23,7 +22,7 @@ type Props = {
type State = Configuration; type State = Configuration;
class HgConfigurationForm extends React.Component<Props, State> { class SvnConfigurationForm extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -76,18 +75,10 @@ class HgConfigurationForm extends React.Component<Props, State> {
onChange={this.handleChange} onChange={this.handleChange}
disabled={readOnly} disabled={readOnly}
/> />
<Checkbox
name="disabled"
label={t("scm-svn-plugin.config.disabled")}
helpText={t("scm-svn-plugin.config.disabledHelpText")}
checked={this.state.disabled}
onChange={this.handleChange}
disabled={readOnly}
/>
</> </>
); );
} }
} }
export default translate("plugins")(HgConfigurationForm); export default translate("plugins")(SvnConfigurationForm);

View File

@@ -36,7 +36,9 @@
"react-i18next": "^7.11.0", "react-i18next": "^7.11.0",
"react-jss": "^8.6.1", "react-jss": "^8.6.1",
"react-router-dom": "^4.3.1", "react-router-dom": "^4.3.1",
"react-select": "^2.1.2" "react-select": "^2.1.2",
"react-markdown": "^4.0.6",
"react-syntax-highlighter": "^10.2.0"
}, },
"resolutions": { "resolutions": {
"gitdiff-parser": "https://github.com/cloudogu/gitdiff-parser#3a72da4a8e3d9bfb4b9e01a43e85628c19f26cc4" "gitdiff-parser": "https://github.com/cloudogu/gitdiff-parser#3a72da4a8e3d9bfb4b9e01a43e85628c19f26cc4"

View File

@@ -26,6 +26,7 @@ type Props = {
selected: (branch?: Branch) => void, selected: (branch?: Branch) => void,
selectedBranch?: string, selectedBranch?: string,
label: string, label: string,
disabled?: boolean,
// context props // context props
classes: Object classes: Object
@@ -47,7 +48,7 @@ class BranchSelector extends React.Component<Props, State> {
} }
render() { render() {
const { branches, classes, label } = this.props; const { branches, classes, label, disabled } = this.props;
if (branches) { if (branches) {
return ( return (
@@ -79,6 +80,7 @@ class BranchSelector extends React.Component<Props, State> {
className="is-fullwidth" className="is-fullwidth"
options={branches.map(b => b.name)} options={branches.map(b => b.name)}
optionSelected={this.branchSelected} optionSelected={this.branchSelected}
disabled={!!disabled}
preselectedOption={ preselectedOption={
this.state.selectedBranch this.state.selectedBranch
? this.state.selectedBranch.name ? this.state.selectedBranch.name

View File

@@ -0,0 +1,50 @@
// @flow
import * as React from "react";
import ErrorNotification from "./ErrorNotification";
type Props = {
fallback?: React.ComponentType<any>,
children: React.Node
};
type ErrorInfo = {
componentStack: string
};
type State = {
error?: Error,
errorInfo?: ErrorInfo
};
class ErrorBoundary extends React.Component<Props,State> {
constructor(props: Props) {
super(props);
this.state = {};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// Catch errors in any components below and re-render with error message
this.setState({
error,
errorInfo
});
}
renderError = () => {
let FallbackComponent = this.props.fallback;
if (!FallbackComponent) {
FallbackComponent = ErrorNotification;
}
return <FallbackComponent {...this.state} />;
};
render() {
const { error } = this.state;
if (error) {
return this.renderError();
}
return this.props.children;
}
}
export default ErrorBoundary;

View File

@@ -0,0 +1,45 @@
//@flow
import React from "react";
import SyntaxHighlighter from "./SyntaxHighlighter";
import Markdown from "react-markdown/with-html";
import {binder} from "@scm-manager/ui-extensions";
type Props = {
content: string,
renderContext?: Object,
renderers?: Object,
};
class MarkdownView extends React.Component<Props> {
render() {
const {content, renderers, renderContext} = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
if (rendererFactory){
rendererList = rendererFactory(renderContext);
}
if (!rendererList){
rendererList = {};
}
if (!rendererList.code){
rendererList.code = SyntaxHighlighter;
}
return (
<Markdown
className="content"
skipHtml={true}
escapeHtml={true}
source={content}
renderers={rendererList}
/>
);
}
}
export default MarkdownView;

View File

@@ -0,0 +1,26 @@
// @flow
import React from "react";
import ReactSyntaxHighlighter from "react-syntax-highlighter";
import { arduinoLight } from "react-syntax-highlighter/dist/styles/hljs";
type Props = {
language: string,
value: string
};
class SyntaxHighlighter extends React.Component<Props> {
render() {
return (
<ReactSyntaxHighlighter
showLineNumbers="false"
language={this.props.language}
style={arduinoLight}
>
{this.props.value}
</ReactSyntaxHighlighter>
);
}
}
export default SyntaxHighlighter;

View File

@@ -1,20 +0,0 @@
//@flow
import React from "react";
type Props = {
content: string
};
class MarkdownView extends React.Component<Props> {
render() {
const {content } = this.props;
return (
<div>
{content}
</div>
);
}
}
export default MarkdownView;

View File

@@ -14,7 +14,8 @@ type Props = {
value?: string, value?: string,
autofocus?: boolean, autofocus?: boolean,
onChange: (value: string, name?: string) => void, onChange: (value: string, name?: string) => void,
helpText?: string helpText?: string,
disabled?: boolean
}; };
class Textarea extends React.Component<Props> { class Textarea extends React.Component<Props> {
@@ -31,7 +32,7 @@ class Textarea extends React.Component<Props> {
}; };
render() { render() {
const { placeholder, value, label, helpText } = this.props; const { placeholder, value, label, helpText, disabled } = this.props;
return ( return (
<div className="field"> <div className="field">
@@ -45,6 +46,7 @@ class Textarea extends React.Component<Props> {
placeholder={placeholder} placeholder={placeholder}
onChange={this.handleInput} onChange={this.handleInput}
value={value} value={value}
disabled={!!disabled}
/> />
</div> </div>
</div> </div>

View File

@@ -11,5 +11,4 @@ export { default as Textarea } from "./Textarea.js";
export { default as PasswordConfirmation } from "./PasswordConfirmation.js"; export { default as PasswordConfirmation } from "./PasswordConfirmation.js";
export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js"; export { default as LabelWithHelpIcon } from "./LabelWithHelpIcon.js";
export { default as DropDown } from "./DropDown.js"; export { default as DropDown } from "./DropDown.js";
export { default as MarkdownView } from "./MarkdownView.js";

View File

@@ -25,6 +25,9 @@ export { default as Tooltip } from "./Tooltip";
export { getPageFromMatch } from "./urls"; export { getPageFromMatch } from "./urls";
export { default as Autocomplete} from "./Autocomplete"; export { default as Autocomplete} from "./Autocomplete";
export { default as BranchSelector } from "./BranchSelector"; export { default as BranchSelector } from "./BranchSelector";
export { default as MarkdownView } from "./MarkdownView";
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
export { default as ErrorBoundary } from "./ErrorBoundary";
export { apiClient } from "./apiclient.js"; export { apiClient } from "./apiclient.js";
export * from "./errors"; export * from "./errors";

View File

@@ -7,6 +7,7 @@ import Subtitle from "./Subtitle";
import injectSheet from "react-jss"; import injectSheet from "react-jss";
import classNames from "classnames"; import classNames from "classnames";
import PageActions from "./PageActions"; import PageActions from "./PageActions";
import ErrorBoundary from "../ErrorBoundary";
type Props = { type Props = {
title?: string, title?: string,
@@ -34,10 +35,13 @@ class Page extends React.Component<Props> {
<section className="section"> <section className="section">
<div className="container"> <div className="container">
{this.renderPageHeader()} {this.renderPageHeader()}
<ErrorNotification error={error} /> <ErrorBoundary>
<ErrorNotification error={error}/>
{this.renderContent()} {this.renderContent()}
</ErrorBoundary>
</div> </div>
</section> </section>
); );
} }
@@ -64,15 +68,15 @@ class Page extends React.Component<Props> {
} }
}); });
let underline = pageActionsExists ? ( let underline = pageActionsExists ? (
<hr className="header-with-actions" /> <hr className="header-with-actions"/>
) : null; ) : null;
return ( return (
<> <>
<div className="columns"> <div className="columns">
<div className="column"> <div className="column">
<Title title={title} /> <Title title={title}/>
<Subtitle subtitle={subtitle} /> <Subtitle subtitle={subtitle}/>
</div> </div>
{pageActions} {pageActions}
</div> </div>
@@ -88,7 +92,7 @@ class Page extends React.Component<Props> {
return null; return null;
} }
if (loading) { if (loading) {
return <Loading />; return <Loading/>;
} }
let content = []; let content = [];

View File

@@ -582,6 +582,12 @@
dependencies: dependencies:
regenerator-runtime "^0.12.0" regenerator-runtime "^0.12.0"
"@babel/runtime@^7.3.1":
version "7.3.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83"
dependencies:
regenerator-runtime "^0.12.0"
"@babel/template@^7.1.0", "@babel/template@^7.1.2": "@babel/template@^7.1.0", "@babel/template@^7.1.2":
version "7.1.2" version "7.1.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.1.2.tgz#090484a574fef5a2d2d7726a674eceda5c5b5644"
@@ -1291,6 +1297,10 @@ backo2@1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
bail@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/bail/-/bail-1.0.3.tgz#63cfb9ddbac829b02a3128cd53224be78e6c21a3"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@@ -1787,6 +1797,18 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0:
escape-string-regexp "^1.0.5" escape-string-regexp "^1.0.5"
supports-color "^5.3.0" supports-color "^5.3.0"
character-entities-legacy@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c"
character-entities@^1.0.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.2.tgz#58c8f371c0774ef0ba9b2aca5f00d8f100e6e363"
character-reference-invalid@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz#21e421ad3d84055952dab4a43a04e73cd425d3ed"
chardet@^0.7.0: chardet@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@@ -1888,6 +1910,14 @@ cli-width@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
clipboard@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d"
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
cliui@^3.2.0: cliui@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
@@ -1944,6 +1974,10 @@ code-point-at@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
collapse-white-space@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/collapse-white-space/-/collapse-white-space-1.0.4.tgz#ce05cf49e54c3277ae573036a26851ba430a0091"
collection-visit@^1.0.0: collection-visit@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -1988,6 +2022,12 @@ combined-stream@^1.0.5, combined-stream@^1.0.6, combined-stream@~1.0.5, combined
dependencies: dependencies:
delayed-stream "~1.0.0" delayed-stream "~1.0.0"
comma-separated-tokens@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db"
dependencies:
trim "0.0.1"
commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0:
version "2.19.0" version "2.19.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a"
@@ -2376,6 +2416,10 @@ delayed-stream@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
delegate@^3.1.2:
version "3.2.0"
resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166"
delegates@^1.0.0: delegates@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2494,6 +2538,10 @@ domelementtype@1, domelementtype@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2"
domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
domelementtype@~1.1.1: domelementtype@~1.1.1:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b"
@@ -2504,7 +2552,7 @@ domexception@^1.0.1:
dependencies: dependencies:
webidl-conversions "^4.0.2" webidl-conversions "^4.0.2"
domhandler@^2.3.0: domhandler@^2.3.0, domhandler@^2.4.2:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
dependencies: dependencies:
@@ -3113,6 +3161,12 @@ fast-xml-parser@^3.12.0:
dependencies: dependencies:
nimnjs "^1.3.2" nimnjs "^1.3.2"
fault@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa"
dependencies:
format "^0.2.2"
fb-watchman@^2.0.0: fb-watchman@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58"
@@ -3322,6 +3376,10 @@ form-data@~2.3.2:
combined-stream "^1.0.6" combined-stream "^1.0.6"
mime-types "^2.1.12" mime-types "^2.1.12"
format@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
fragment-cache@^0.2.1: fragment-cache@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -3593,6 +3651,12 @@ glogg@^1.0.0:
dependencies: dependencies:
sparkles "^1.0.0" sparkles "^1.0.0"
good-listener@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50"
dependencies:
delegate "^3.1.2"
got@^7.1.0: got@^7.1.0:
version "7.1.0" version "7.1.0"
resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a"
@@ -3831,6 +3895,19 @@ hash.js@^1.0.0, hash.js@^1.0.3:
inherits "^2.0.3" inherits "^2.0.3"
minimalistic-assert "^1.0.1" minimalistic-assert "^1.0.1"
hast-util-parse-selector@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.1.tgz#4ddbae1ae12c124e3eb91b581d2556441766f0ab"
hastscript@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-5.0.0.tgz#fee10382c1bc4ba3f1be311521d368c047d2c43a"
dependencies:
comma-separated-tokens "^1.0.0"
hast-util-parse-selector "^2.2.0"
property-information "^5.0.1"
space-separated-tokens "^1.0.0"
hawk@~3.1.3: hawk@~3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4"
@@ -3840,6 +3917,10 @@ hawk@~3.1.3:
hoek "2.x.x" hoek "2.x.x"
sntp "1.x.x" sntp "1.x.x"
highlight.js@~9.13.0:
version "9.13.1"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.13.1.tgz#054586d53a6863311168488a0f58d6c505ce641e"
history@^4.7.2: history@^4.7.2:
version "4.7.2" version "4.7.2"
resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
@@ -3895,10 +3976,31 @@ html-parse-stringify2@2.0.1:
dependencies: dependencies:
void-elements "^2.0.1" void-elements "^2.0.1"
html-to-react@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.3.4.tgz#647b3a54fdec73a6461864b129fb0d1eec7d4589"
dependencies:
domhandler "^2.4.2"
escape-string-regexp "^1.0.5"
htmlparser2 "^3.10.0"
lodash.camelcase "^4.3.0"
ramda "^0.26"
htmlescape@^1.1.0: htmlescape@^1.1.0:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351" resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
htmlparser2@^3.10.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
dependencies:
domelementtype "^1.3.1"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^3.1.1"
htmlparser2@^3.9.1: htmlparser2@^3.9.1:
version "3.9.2" version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -4111,6 +4213,17 @@ is-accessor-descriptor@^1.0.0:
dependencies: dependencies:
kind-of "^6.0.0" kind-of "^6.0.0"
is-alphabetical@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41"
is-alphanumerical@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz#1138e9ae5040158dc6ff76b820acd6b7a181fd40"
dependencies:
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-arrayish@^0.2.1: is-arrayish@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
@@ -4125,7 +4238,7 @@ is-boolean-object@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.0.tgz#98f8b28030684219a95f375cfbd88ce3405dff93"
is-buffer@^1.1.0, is-buffer@^1.1.5, is-buffer@~1.1.1: is-buffer@^1.1.0, is-buffer@^1.1.4, is-buffer@^1.1.5, is-buffer@~1.1.1:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be"
@@ -4161,6 +4274,10 @@ is-date-object@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16"
is-decimal@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff"
is-descriptor@^0.1.0: is-descriptor@^0.1.0:
version "0.1.6" version "0.1.6"
resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca"
@@ -4251,6 +4368,10 @@ is-glob@^4.0.0:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-hexadecimal@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835"
is-in-browser@^1.0.2, is-in-browser@^1.1.3: is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
@@ -4397,6 +4518,10 @@ is-utf8@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
is-whitespace-character@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz#ede53b4c6f6fb3874533751ec9280d01928d03ed"
is-windows@^0.2.0: is-windows@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
@@ -4405,6 +4530,10 @@ is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
is-word-character@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-word-character/-/is-word-character-1.0.2.tgz#46a5dac3f2a1840898b91e576cd40d493f3ae553"
is-wsl@^1.1.0: is-wsl@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d"
@@ -5187,6 +5316,10 @@ lodash.assign@^4.0.3, lodash.assign@^4.0.6:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
lodash.camelcase@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
lodash.debounce@^4.0.8: lodash.debounce@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
@@ -5292,6 +5425,13 @@ lowercase-keys@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
lowlight@~1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.11.0.tgz#1304d83005126d4e8b1dc0f07981e9b689ec2efc"
dependencies:
fault "^1.0.2"
highlight.js "~9.13.0"
lru-cache@2: lru-cache@2:
version "2.7.3" version "2.7.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
@@ -5345,6 +5485,10 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" object-visit "^1.0.0"
markdown-escapes@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.2.tgz#e639cbde7b99c841c0bacc8a07982873b46d2122"
math-random@^1.0.1: math-random@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac"
@@ -5365,6 +5509,12 @@ md5@^2.1.0:
crypt "~0.0.1" crypt "~0.0.1"
is-buffer "~1.1.1" is-buffer "~1.1.1"
mdast-add-list-metadata@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz#95e73640ce2fc1fa2dcb7ec443d09e2bfe7db4cf"
dependencies:
unist-util-visit-parents "1.1.2"
mem@^1.1.0: mem@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76"
@@ -6049,6 +6199,17 @@ parse-asn1@^5.0.0:
evp_bytestokey "^1.0.0" evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3" pbkdf2 "^3.0.3"
parse-entities@^1.1.0, parse-entities@^1.1.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.1.tgz#2c761ced065ba7dc68148580b5a225e4918cdd69"
dependencies:
character-entities "^1.0.0"
character-entities-legacy "^1.0.0"
character-reference-invalid "^1.0.0"
is-alphanumerical "^1.0.0"
is-decimal "^1.0.0"
is-hexadecimal "^1.0.0"
parse-filepath@^1.0.1: parse-filepath@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891"
@@ -6280,6 +6441,12 @@ pretty-hrtime@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" resolved "http://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
prismjs@^1.8.4, prismjs@~1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9"
optionalDependencies:
clipboard "^2.0.0"
private@^0.1.6, private@^0.1.8: private@^0.1.6, private@^0.1.8:
version "0.1.8" version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -6314,6 +6481,12 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2:
loose-envify "^1.3.1" loose-envify "^1.3.1"
object-assign "^4.1.1" object-assign "^4.1.1"
property-information@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.0.1.tgz#c3b09f4f5750b1634c0b24205adbf78f18bdf94f"
dependencies:
xtend "^4.0.1"
pseudomap@^1.0.2: pseudomap@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
@@ -6375,6 +6548,10 @@ railroad-diagrams@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
ramda@^0.26:
version "0.26.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.26.1.tgz#8d41351eb8111c55353617fc3bbffad8e4d35d06"
randexp@0.4.6: randexp@0.4.6:
version "0.4.6" version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3" resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
@@ -6487,6 +6664,18 @@ react-lifecycles-compat@^3.0.4:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
react-markdown@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-4.0.6.tgz#927d44421735cd90b7634bb221e9d7d8656e01e9"
dependencies:
html-to-react "^1.3.4"
mdast-add-list-metadata "1.0.1"
prop-types "^15.6.1"
remark-parse "^5.0.0"
unified "^6.1.5"
unist-util-visit "^1.3.0"
xtend "^4.0.1"
react-router-dom@^4.3.1: react-router-dom@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6"
@@ -6529,6 +6718,16 @@ react-select@^2.1.2:
react-input-autosize "^2.2.1" react-input-autosize "^2.2.1"
react-transition-group "^2.2.1" react-transition-group "^2.2.1"
react-syntax-highlighter@^10.2.0:
version "10.2.0"
resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-10.2.0.tgz#c44c2a1e89326bbd808cf7a8b2f08dc5ed025d91"
dependencies:
"@babel/runtime" "^7.3.1"
highlight.js "~9.13.0"
lowlight "~1.11.0"
prismjs "^1.8.4"
refractor "^2.4.1"
react-test-renderer@^16.0.0-0: react-test-renderer@^16.0.0-0:
version "16.5.2" version "16.5.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae"
@@ -6622,6 +6821,14 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
string_decoder "~1.1.1" string_decoder "~1.1.1"
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
readable-stream@^3.1.1:
version "3.2.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@~1.1.9: readable-stream@~1.1.9:
version "1.1.14" version "1.1.14"
resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -6663,6 +6870,14 @@ rechoir@^0.6.2:
dependencies: dependencies:
resolve "^1.1.6" resolve "^1.1.6"
refractor@^2.4.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.7.0.tgz#3ed9a96a619e75326a429e644241dea51be070a3"
dependencies:
hastscript "^5.0.0"
parse-entities "^1.1.2"
prismjs "~1.15.0"
regenerate-unicode-properties@^7.0.0: regenerate-unicode-properties@^7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c"
@@ -6729,6 +6944,26 @@ regjsparser@^0.3.0:
dependencies: dependencies:
jsesc "~0.5.0" jsesc "~0.5.0"
remark-parse@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-5.0.0.tgz#4c077f9e499044d1d5c13f80d7a98cf7b9285d95"
dependencies:
collapse-white-space "^1.0.2"
is-alphabetical "^1.0.0"
is-decimal "^1.0.0"
is-whitespace-character "^1.0.0"
is-word-character "^1.0.0"
markdown-escapes "^1.0.0"
parse-entities "^1.1.0"
repeat-string "^1.5.4"
state-toggle "^1.0.0"
trim "0.0.1"
trim-trailing-lines "^1.0.0"
unherit "^1.0.4"
unist-util-remove-position "^1.0.0"
vfile-location "^2.0.0"
xtend "^4.0.1"
remove-trailing-separator@^1.0.1: remove-trailing-separator@^1.0.1:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef"
@@ -6737,7 +6972,7 @@ repeat-element@^1.1.2:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
repeat-string@^1.5.2, repeat-string@^1.6.1: repeat-string@^1.5.2, repeat-string@^1.5.4, repeat-string@^1.6.1:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
@@ -6751,7 +6986,7 @@ replace-ext@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
replace-ext@^1.0.0: replace-ext@1.0.0, replace-ext@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
@@ -6994,6 +7229,10 @@ scheduler@^0.10.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
object-assign "^4.1.1" object-assign "^4.1.1"
select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d"
"semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: "semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
@@ -7256,6 +7495,12 @@ source-map@^0.7.2:
version "0.7.3" version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
space-separated-tokens@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412"
dependencies:
trim "0.0.1"
sparkles@^1.0.0: sparkles@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c"
@@ -7310,6 +7555,10 @@ stack-utils@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
state-toggle@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.1.tgz#c3cb0974f40a6a0f8e905b96789eb41afa1cde3a"
static-extend@^0.1.1: static-extend@^0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@@ -7625,6 +7874,10 @@ timers-ext@^0.1.5:
es5-ext "~0.10.46" es5-ext "~0.10.46"
next-tick "1" next-tick "1"
tiny-emitter@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423"
tmp@^0.0.33: tmp@^0.0.33:
version "0.0.33" version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@@ -7710,6 +7963,18 @@ trim-right@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
trim-trailing-lines@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz#e0ec0810fd3c3f1730516b45f49083caaf2774d9"
trim@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd"
trough@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.3.tgz#e29bd1614c6458d44869fc28b255ab7857ef7c24"
tslib@^1.9.0: tslib@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
@@ -7778,6 +8043,13 @@ underscore@~1.4.4:
version "1.4.4" version "1.4.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
unherit@^1.0.4:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unherit/-/unherit-1.1.1.tgz#132748da3e88eab767e08fabfbb89c5e9d28628c"
dependencies:
inherits "^2.0.1"
xtend "^4.0.1"
unicode-canonical-property-names-ecmascript@^1.0.4: unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@@ -7797,6 +8069,17 @@ unicode-property-aliases-ecmascript@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0" resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.4.tgz#5a533f31b4317ea76f17d807fa0d116546111dd0"
unified@^6.1.5:
version "6.2.0"
resolved "https://registry.yarnpkg.com/unified/-/unified-6.2.0.tgz#7fbd630f719126d67d40c644b7e3f617035f6dba"
dependencies:
bail "^1.0.0"
extend "^3.0.0"
is-plain-obj "^1.1.0"
trough "^1.0.0"
vfile "^2.0.0"
x-is-string "^0.1.0"
union-value@^1.0.0: union-value@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4"
@@ -7810,6 +8093,36 @@ unique-stream@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b"
unist-util-is@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-2.1.2.tgz#1193fa8f2bfbbb82150633f3a8d2eb9a1c1d55db"
unist-util-remove-position@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz#86b5dad104d0bbfbeb1db5f5c92f3570575c12cb"
dependencies:
unist-util-visit "^1.1.0"
unist-util-stringify-position@^1.0.0, unist-util-stringify-position@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz#3f37fcf351279dcbca7480ab5889bb8a832ee1c6"
unist-util-visit-parents@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz#f6e3afee8bdbf961c0e6f028ea3c0480028c3d06"
unist-util-visit-parents@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-2.0.1.tgz#63fffc8929027bee04bfef7d2cce474f71cb6217"
dependencies:
unist-util-is "^2.1.2"
unist-util-visit@^1.1.0, unist-util-visit@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-1.4.0.tgz#1cb763647186dc26f5e1df5db6bd1e48b3cc2fb1"
dependencies:
unist-util-visit-parents "^2.0.0"
universal-user-agent@^2.0.0: universal-user-agent@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.1.tgz#18e591ca52b1cb804f6b9cbc4c336cf8191f80e1" resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.1.tgz#18e591ca52b1cb804f6b9cbc4c336cf8191f80e1"
@@ -7888,7 +8201,7 @@ user-home@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
util-deprecate@~1.0.1: util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -7944,6 +8257,25 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
vfile-location@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-2.0.4.tgz#2a5e7297dd0d9e2da4381464d04acc6b834d3e55"
vfile-message@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-1.1.1.tgz#5833ae078a1dfa2d96e9647886cd32993ab313e1"
dependencies:
unist-util-stringify-position "^1.1.1"
vfile@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/vfile/-/vfile-2.3.0.tgz#e62d8e72b20e83c324bc6c67278ee272488bf84a"
dependencies:
is-buffer "^1.1.4"
replace-ext "1.0.0"
unist-util-stringify-position "^1.0.0"
vfile-message "^1.0.0"
vinyl-buffer@^1.0.1: vinyl-buffer@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/vinyl-buffer/-/vinyl-buffer-1.0.1.tgz#96c1a3479b8c5392542c612029013b5b27f88bbf" resolved "https://registry.yarnpkg.com/vinyl-buffer/-/vinyl-buffer-1.0.1.tgz#96c1a3479b8c5392542c612029013b5b27f88bbf"
@@ -8161,6 +8493,10 @@ ws@~3.3.1:
safe-buffer "~5.1.0" safe-buffer "~5.1.0"
ultron "~1.1.0" ultron "~1.1.0"
x-is-string@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"

View File

@@ -20,6 +20,6 @@ export type Config = {
pluginUrl: string, pluginUrl: string,
loginAttemptLimitTimeout: number, loginAttemptLimitTimeout: number,
enabledXsrfProtection: boolean, enabledXsrfProtection: boolean,
defaultNamespaceStrategy: string, namespaceStrategy: string,
_links: Links _links: Links
}; };

View File

@@ -0,0 +1,9 @@
// @flow
import type { Links } from "./hal";
export type NamespaceStrategies = {
current: string,
available: string[],
_links: Links
};

View File

@@ -26,3 +26,5 @@ export type { SubRepository, File } from "./Sources";
export type { SelectValue, AutocompleteObject } from "./Autocomplete"; export type { SelectValue, AutocompleteObject } from "./Autocomplete";
export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions"; export type { AvailableRepositoryPermissions, RepositoryRole } from "./AvailableRepositoryPermissions";
export type { NamespaceStrategies } from "./NamespaceStrategies";

File diff suppressed because it is too large Load Diff

View File

@@ -44,7 +44,7 @@
"skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen", "skip-failed-authenticators": "Fehlgeschlagene Authentifizierer überspringen",
"plugin-url": "Plugin URL", "plugin-url": "Plugin URL",
"enabled-xsrf-protection": "XSRF Protection aktivieren", "enabled-xsrf-protection": "XSRF Protection aktivieren",
"default-namespace-strategy": "Default Namespace Strategie" "namespace-strategy": "Namespace Strategie"
}, },
"validation": { "validation": {
"date-format-invalid": "Das Datumsformat ist ungültig", "date-format-invalid": "Das Datumsformat ist ungültig",
@@ -74,6 +74,6 @@
"proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.", "proxyUserHelpText": "Der Benutzername für die Proxy Server Anmeldung.",
"proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.", "proxyExcludesHelpText": "Glob patterns für Hostnamen, die von den Proxy-Einstellungen ausgeschlossen werden sollen.",
"enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.", "enableXsrfProtectionHelpText": "Xsrf Cookie Protection aktivieren. Hinweis: Dieses Feature befindet sich noch im Experimentalstatus.",
"defaultNameSpaceStrategyHelpText": "Die Standardstrategie für Namespaces." "nameSpaceStrategyHelpText": "Strategie für Namespaces."
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"repository": { "repository": {
"namespace": "Namespace",
"name": "Name", "name": "Name",
"type": "Typ", "type": "Typ",
"contact": "Kontakt", "contact": "Kontakt",
@@ -8,10 +9,12 @@
"lastModified": "Zuletzt bearbeitet" "lastModified": "Zuletzt bearbeitet"
}, },
"validation": { "validation": {
"namespace-invalid": "Der Namespace des Repository ist ungültig",
"name-invalid": "Der Name des Repository ist ungültig", "name-invalid": "Der Name des Repository ist ungültig",
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein" "contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein"
}, },
"help": { "help": {
"namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.",
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.", "nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
"typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).", "typeHelpText": "Der Typ des Repository (Mercurial, Git oder Subversion).",
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.", "contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",

View File

@@ -44,7 +44,7 @@
"skip-failed-authenticators": "Skip Failed Authenticators", "skip-failed-authenticators": "Skip Failed Authenticators",
"plugin-url": "Plugin URL", "plugin-url": "Plugin URL",
"enabled-xsrf-protection": "Enabled XSRF Protection", "enabled-xsrf-protection": "Enabled XSRF Protection",
"default-namespace-strategy": "Default Namespace Strategy" "namespace-strategy": "Namespace Strategy"
}, },
"validation": { "validation": {
"date-format-invalid": "The date format is not valid", "date-format-invalid": "The date format is not valid",
@@ -74,6 +74,6 @@
"proxyUserHelpText": "The username for the proxy server authentication.", "proxyUserHelpText": "The username for the proxy server authentication.",
"proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.", "proxyExcludesHelpText": "Glob patterns for hostnames, which should be excluded from proxy settings.",
"enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.", "enableXsrfProtectionHelpText": "Enable XSRF Cookie Protection. Note: This feature is still experimental.",
"defaultNameSpaceStrategyHelpText": "The default namespace strategy." "nameSpaceStrategyHelpText": "The namespace strategy."
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"repository": { "repository": {
"namespace": "Namespace",
"name": "Name", "name": "Name",
"type": "Type", "type": "Type",
"contact": "Contact", "contact": "Contact",
@@ -8,10 +9,12 @@
"lastModified": "Last Modified" "lastModified": "Last Modified"
}, },
"validation": { "validation": {
"namespace-invalid": "The repository namespace is invalid",
"name-invalid": "The repository name is invalid", "name-invalid": "The repository name is invalid",
"contact-invalid": "Contact must be a valid mail address" "contact-invalid": "Contact must be a valid mail address"
}, },
"help": { "help": {
"namespaceHelpText": "The namespace of the repository. This name will be part of the repository url.",
"nameHelpText": "The name of the repository. This name will be part of the repository url.", "nameHelpText": "The name of the repository. This name will be part of the repository url.",
"typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).", "typeHelpText": "The type of the repository (e.g. Mercurial, Git or Subversion).",
"contactHelpText": "Email address of the person who is responsible for this repository.", "contactHelpText": "Email address of the person who is responsible for this repository.",

View File

@@ -2,7 +2,7 @@
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { SubmitButton, Notification } from "@scm-manager/ui-components"; import { SubmitButton, Notification } from "@scm-manager/ui-components";
import type { Config } from "@scm-manager/ui-types"; import type { NamespaceStrategies, Config } from "@scm-manager/ui-types";
import ProxySettings from "./ProxySettings"; import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings"; import GeneralSettings from "./GeneralSettings";
import BaseUrlSettings from "./BaseUrlSettings"; import BaseUrlSettings from "./BaseUrlSettings";
@@ -12,9 +12,11 @@ type Props = {
submitForm: Config => void, submitForm: Config => void,
config?: Config, config?: Config,
loading?: boolean, loading?: boolean,
t: string => string,
configReadPermission: boolean, configReadPermission: boolean,
configUpdatePermission: boolean configUpdatePermission: boolean,
namespaceStrategies?: NamespaceStrategies,
// context props
t: string => string,
}; };
type State = { type State = {
@@ -51,7 +53,7 @@ class ConfigForm extends React.Component<Props, State> {
pluginUrl: "", pluginUrl: "",
loginAttemptLimitTimeout: 0, loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true, enabledXsrfProtection: true,
defaultNamespaceStrategy: "", namespaceStrategy: "",
_links: {} _links: {}
}, },
showNotification: false, showNotification: false,
@@ -85,6 +87,7 @@ class ConfigForm extends React.Component<Props, State> {
const { const {
loading, loading,
t, t,
namespaceStrategies,
configReadPermission, configReadPermission,
configUpdatePermission configUpdatePermission
} = this.props; } = this.props;
@@ -115,6 +118,7 @@ class ConfigForm extends React.Component<Props, State> {
<form onSubmit={this.submit}> <form onSubmit={this.submit}>
{noPermissionNotification} {noPermissionNotification}
<GeneralSettings <GeneralSettings
namespaceStrategies={namespaceStrategies}
realmDescription={config.realmDescription} realmDescription={config.realmDescription}
enableRepositoryArchive={config.enableRepositoryArchive} enableRepositoryArchive={config.enableRepositoryArchive}
disableGroupingGrid={config.disableGroupingGrid} disableGroupingGrid={config.disableGroupingGrid}
@@ -123,7 +127,7 @@ class ConfigForm extends React.Component<Props, State> {
skipFailedAuthenticators={config.skipFailedAuthenticators} skipFailedAuthenticators={config.skipFailedAuthenticators}
pluginUrl={config.pluginUrl} pluginUrl={config.pluginUrl}
enabledXsrfProtection={config.enabledXsrfProtection} enabledXsrfProtection={config.enabledXsrfProtection}
defaultNamespaceStrategy={config.defaultNamespaceStrategy} namespaceStrategy={config.namespaceStrategy}
onChange={(isValid, changedValue, name) => onChange={(isValid, changedValue, name) =>
this.onChange(isValid, changedValue, name) this.onChange(isValid, changedValue, name)
} }

View File

@@ -2,6 +2,8 @@
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Checkbox, InputField } from "@scm-manager/ui-components"; import { Checkbox, InputField } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = { type Props = {
realmDescription: string, realmDescription: string,
@@ -12,10 +14,12 @@ type Props = {
skipFailedAuthenticators: boolean, skipFailedAuthenticators: boolean,
pluginUrl: string, pluginUrl: string,
enabledXsrfProtection: boolean, enabledXsrfProtection: boolean,
defaultNamespaceStrategy: string, namespaceStrategy: string,
t: string => string, namespaceStrategies?: NamespaceStrategies,
onChange: (boolean, any, string) => void, onChange: (boolean, any, string) => void,
hasUpdatePermission: boolean hasUpdatePermission: boolean,
// context props
t: string => string
}; };
class GeneralSettings extends React.Component<Props> { class GeneralSettings extends React.Component<Props> {
@@ -23,15 +27,10 @@ class GeneralSettings extends React.Component<Props> {
const { const {
t, t,
realmDescription, realmDescription,
enableRepositoryArchive,
disableGroupingGrid,
dateFormat,
anonymousAccessEnabled,
skipFailedAuthenticators,
pluginUrl,
enabledXsrfProtection, enabledXsrfProtection,
defaultNamespaceStrategy, namespaceStrategy,
hasUpdatePermission hasUpdatePermission,
namespaceStrategies
} = this.props; } = this.props;
return ( return (
@@ -47,32 +46,13 @@ class GeneralSettings extends React.Component<Props> {
/> />
</div> </div>
<div className="column is-half"> <div className="column is-half">
<InputField <NamespaceStrategySelect
label={t("general-settings.date-format")} label={t("general-settings.namespace-strategy")}
onChange={this.handleDateFormatChange} onChange={this.handleNamespaceStrategyChange}
value={dateFormat} value={namespaceStrategy}
disabled={!hasUpdatePermission} disabled={!hasUpdatePermission}
helpText={t("help.dateFormatHelpText")} namespaceStrategies={namespaceStrategies}
/> helpText={t("help.nameSpaceStrategyHelpText")}
</div>
</div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.plugin-url")}
onChange={this.handlePluginUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginRepositoryHelpText")}
/>
</div>
<div className="column is-half">
<InputField
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/> />
</div> </div>
</div> </div>
@@ -86,46 +66,6 @@ class GeneralSettings extends React.Component<Props> {
helpText={t("help.enableXsrfProtectionHelpText")} helpText={t("help.enableXsrfProtectionHelpText")}
/> />
</div> </div>
<div className="column is-half">
<Checkbox
checked={enableRepositoryArchive}
label={t("general-settings.enable-repository-archive")}
onChange={this.handleEnableRepositoryArchiveChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableRepositoryArchiveHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={disableGroupingGrid}
label={t("general-settings.disable-grouping-grid")}
onChange={this.handleDisableGroupingGridChange}
disabled={!hasUpdatePermission}
helpText={t("help.disableGroupingGridHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
checked={anonymousAccessEnabled}
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleAnonymousAccessEnabledChange}
disabled={!hasUpdatePermission}
helpText={t("help.allowAnonymousAccessHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={skipFailedAuthenticators}
label={t("general-settings.skip-failed-authenticators")}
onChange={this.handleSkipFailedAuthenticatorsChange}
disabled={!hasUpdatePermission}
helpText={t("help.skipFailedAuthenticatorsHelpText")}
/>
</div>
</div> </div>
</div> </div>
); );
@@ -134,31 +74,11 @@ class GeneralSettings extends React.Component<Props> {
handleRealmDescriptionChange = (value: string) => { handleRealmDescriptionChange = (value: string) => {
this.props.onChange(true, value, "realmDescription"); this.props.onChange(true, value, "realmDescription");
}; };
handleEnableRepositoryArchiveChange = (value: boolean) => {
this.props.onChange(true, value, "enableRepositoryArchive");
};
handleDisableGroupingGridChange = (value: boolean) => {
this.props.onChange(true, value, "disableGroupingGrid");
};
handleDateFormatChange = (value: string) => {
this.props.onChange(true, value, "dateFormat");
};
handleAnonymousAccessEnabledChange = (value: string) => {
this.props.onChange(true, value, "anonymousAccessEnabled");
};
handleSkipFailedAuthenticatorsChange = (value: string) => {
this.props.onChange(true, value, "skipFailedAuthenticators");
};
handlePluginUrlChange = (value: string) => {
this.props.onChange(true, value, "pluginUrl");
};
handleEnabledXsrfProtectionChange = (value: boolean) => { handleEnabledXsrfProtectionChange = (value: boolean) => {
this.props.onChange(true, value, "enabledXsrfProtection"); this.props.onChange(true, value, "enabledXsrfProtection");
}; };
handleDefaultNamespaceStrategyChange = (value: string) => { handleNamespaceStrategyChange = (value: string) => {
this.props.onChange(true, value, "defaultNamespaceStrategy"); this.props.onChange(true, value, "namespaceStrategy");
}; };
} }

View File

@@ -0,0 +1,67 @@
//@flow
import React from "react";
import { translate, type TFunction } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
type Props = {
namespaceStrategies: NamespaceStrategies,
label: string,
value?: string,
disabled?: boolean,
helpText?: string,
onChange: (value: string, name?: string) => void,
// context props
t: TFunction
};
class NamespaceStrategySelect extends React.Component<Props> {
createNamespaceOptions = () => {
const { namespaceStrategies, t } = this.props;
let available = [];
if (namespaceStrategies && namespaceStrategies.available) {
available = namespaceStrategies.available;
}
return available.map(ns => {
const key = "namespaceStrategies." + ns;
let label = t(key);
if (label === key) {
label = ns;
}
return {
value: ns,
label: label
};
});
};
findSelected = () => {
const { namespaceStrategies, value } = this.props;
if (
!namespaceStrategies ||
!namespaceStrategies.available ||
namespaceStrategies.available.indexOf(value) < 0
) {
return namespaceStrategies.current;
}
return value;
};
render() {
const { label, helpText, disabled, onChange } = this.props;
const nsOptions = this.createNamespaceOptions();
return (
<Select
label={label}
onChange={onChange}
value={this.findSelected()}
disabled={disabled}
options={nsOptions}
helpText={helpText}
/>
);
}
}
export default translate("plugins")(NamespaceStrategySelect);

View File

@@ -14,9 +14,15 @@ import {
modifyConfigReset modifyConfigReset
} from "../modules/config"; } from "../modules/config";
import { connect } from "react-redux"; import { connect } from "react-redux";
import type { Config } from "@scm-manager/ui-types"; import type { Config, NamespaceStrategies } from "@scm-manager/ui-types";
import ConfigForm from "../components/form/ConfigForm"; import ConfigForm from "../components/form/ConfigForm";
import { getConfigLink } from "../../modules/indexResource"; import { getConfigLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
getFetchNamespaceStrategiesFailure,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending
} from "../modules/namespaceStrategies";
type Props = { type Props = {
loading: boolean, loading: boolean,
@@ -24,11 +30,13 @@ type Props = {
config: Config, config: Config,
configUpdatePermission: boolean, configUpdatePermission: boolean,
configLink: string, configLink: string,
namespaceStrategies?: NamespaceStrategies,
// dispatch functions // dispatch functions
modifyConfig: (config: Config, callback?: () => void) => void, modifyConfig: (config: Config, callback?: () => void) => void,
fetchConfig: (link: string) => void, fetchConfig: (link: string) => void,
configReset: void => void, configReset: void => void,
fetchNamespaceStrategiesIfNeeded: void => void,
// context objects // context objects
t: string => string t: string => string
@@ -51,6 +59,7 @@ class GlobalConfig extends React.Component<Props, State> {
componentDidMount() { componentDidMount() {
this.props.configReset(); this.props.configReset();
this.props.fetchNamespaceStrategiesIfNeeded();
if (this.props.configLink) { if (this.props.configLink) {
this.props.fetchConfig(this.props.configLink); this.props.fetchConfig(this.props.configLink);
} else { } else {
@@ -103,7 +112,7 @@ class GlobalConfig extends React.Component<Props, State> {
}; };
renderContent = () => { renderContent = () => {
const { error, loading, config, configUpdatePermission } = this.props; const { error, loading, config, configUpdatePermission, namespaceStrategies } = this.props;
const { configReadPermission } = this.state; const { configReadPermission } = this.state;
if (!error) { if (!error) {
return ( return (
@@ -113,6 +122,7 @@ class GlobalConfig extends React.Component<Props, State> {
submitForm={config => this.modifyConfig(config)} submitForm={config => this.modifyConfig(config)}
config={config} config={config}
loading={loading} loading={loading}
namespaceStrategies={namespaceStrategies}
configUpdatePermission={configUpdatePermission} configUpdatePermission={configUpdatePermission}
configReadPermission={configReadPermission} configReadPermission={configReadPermission}
/> />
@@ -133,23 +143,33 @@ const mapDispatchToProps = dispatch => {
}, },
configReset: () => { configReset: () => {
dispatch(modifyConfigReset()); dispatch(modifyConfigReset());
},
fetchNamespaceStrategiesIfNeeded: () => {
dispatch(fetchNamespaceStrategiesIfNeeded());
} }
}; };
}; };
const mapStateToProps = state => { const mapStateToProps = state => {
const loading = isFetchConfigPending(state) || isModifyConfigPending(state); const loading = isFetchConfigPending(state)
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); || isModifyConfigPending(state)
|| isFetchNamespaceStrategiesPending(state);
const error = getFetchConfigFailure(state)
|| getModifyConfigFailure(state)
|| getFetchNamespaceStrategiesFailure(state);
const config = getConfig(state); const config = getConfig(state);
const configUpdatePermission = getConfigUpdatePermission(state); const configUpdatePermission = getConfigUpdatePermission(state);
const configLink = getConfigLink(state); const configLink = getConfigLink(state);
const namespaceStrategies = getNamespaceStrategies(state);
return { return {
loading, loading,
error, error,
config, config,
configUpdatePermission, configUpdatePermission,
configLink configLink,
namespaceStrategies
}; };
}; };

View File

@@ -50,7 +50,7 @@ const config = {
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
loginAttemptLimitTimeout: 300, loginAttemptLimitTimeout: 300,
enabledXsrfProtection: true, enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", namespaceStrategy: "UsernameNamespaceStrategy",
_links: { _links: {
self: { href: "http://localhost:8081/api/v2/config" }, self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" } update: { href: "http://localhost:8081/api/v2/config" }
@@ -79,7 +79,7 @@ const configWithNullValues = {
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false", "http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
loginAttemptLimitTimeout: 300, loginAttemptLimitTimeout: 300,
enabledXsrfProtection: true, enabledXsrfProtection: true,
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy", namespaceStrategy: "UsernameNamespaceStrategy",
_links: { _links: {
self: { href: "http://localhost:8081/api/v2/config" }, self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" } update: { href: "http://localhost:8081/api/v2/config" }

View File

@@ -0,0 +1,115 @@
// @flow
import * as types from "../../modules/types";
import type { Action, NamespaceStrategies } from "@scm-manager/ui-types";
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import { MODIFY_CONFIG_SUCCESS } from "./config";
export const FETCH_NAMESPACESTRATEGIES_TYPES =
"scm/config/FETCH_NAMESPACESTRATEGIES_TYPES";
export const FETCH_NAMESPACESTRATEGIES_TYPES_PENDING = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.PENDING_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.FAILURE_SUFFIX
}`;
export function fetchNamespaceStrategiesIfNeeded() {
return function(dispatch: any, getState: () => Object) {
const state = getState();
if (shouldFetchNamespaceStrategies(state)) {
return fetchNamespaceStrategies(
dispatch,
state.indexResources.links.namespaceStrategies.href
);
}
};
}
function fetchNamespaceStrategies(dispatch: any, url: string) {
dispatch(fetchNamespaceStrategiesPending());
return apiClient
.get(url)
.then(response => response.json())
.then(namespaceStrategies => {
dispatch(fetchNamespaceStrategiesSuccess(namespaceStrategies));
})
.catch(error => {
dispatch(fetchNamespaceStrategiesFailure(error));
});
}
export function shouldFetchNamespaceStrategies(state: Object) {
if (
isFetchNamespaceStrategiesPending(state) ||
getFetchNamespaceStrategiesFailure(state)
) {
return false;
}
return !state.namespaceStrategies || !state.namespaceStrategies.current;
}
export function fetchNamespaceStrategiesPending(): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING
};
}
export function fetchNamespaceStrategiesSuccess(
namespaceStrategies: NamespaceStrategies
): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: namespaceStrategies
};
}
export function fetchNamespaceStrategiesFailure(error: Error): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
payload: error
};
}
// reducers
export default function reducer(
state: Object = {},
action: Action = { type: "UNKNOWN" }
): Object {
if (
action.type === FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS &&
action.payload
) {
return action.payload;
} else if (action.type === MODIFY_CONFIG_SUCCESS && action.payload) {
const config = action.payload;
return {
...state,
current: config.namespaceStrategy
};
}
return state;
}
// selectors
export function getNamespaceStrategies(state: Object) {
if (state.namespaceStrategies) {
return state.namespaceStrategies;
}
return {};
}
export function isFetchNamespaceStrategiesPending(state: Object) {
return isPending(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}
export function getFetchNamespaceStrategiesFailure(state: Object) {
return getFailure(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}

View File

@@ -0,0 +1,199 @@
// @flow
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import {
FETCH_NAMESPACESTRATEGIES_TYPES,
FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
FETCH_NAMESPACESTRATEGIES_TYPES_PENDING,
FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
fetchNamespaceStrategiesIfNeeded,
fetchNamespaceStrategiesSuccess,
shouldFetchNamespaceStrategies,
default as reducer,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending,
getFetchNamespaceStrategiesFailure
} from "./namespaceStrategies";
import { MODIFY_CONFIG_SUCCESS } from "./config";
const strategies = {
current: "UsernameNamespaceStrategy",
available: [
"UsernameNamespaceStrategy",
"CustomNamespaceStrategy",
"CurrentYearNamespaceStrategy",
"RepositoryTypeNamespaceStrategy"
],
_links: {
self: {
href: "http://localhost:8081/scm/api/v2/namespaceStrategies"
}
}
};
describe("namespace strategy caching", () => {
it("should fetch strategies, on empty state", () => {
expect(shouldFetchNamespaceStrategies({})).toBe(true);
});
it("should fetch strategies, on empty namespaceStrategies node", () => {
const state = {
namespaceStrategies: {}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(true);
});
it("should not fetch strategies, on pending state", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, on failure state", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: new Error("no...")
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, if they are already fetched", () => {
const state = {
namespaceStrategies: {
current: "some"
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
});
describe("namespace strategies fetch", () => {
const URL = "http://scm.hitchhiker.com/api/v2/namespaceStrategies";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
const createStore = (initialState = {}) => {
return mockStore({
...initialState,
indexResources: {
links: {
namespaceStrategies: {
href: URL
}
}
}
});
};
it("should successfully fetch strategies", () => {
fetchMock.getOnce(URL, strategies);
const expectedActions = [
{ type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING },
{
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: strategies
}
];
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE on server error", () => {
fetchMock.getOnce(URL, {
status: 500
});
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_PENDING);
expect(actions[1].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should not dispatch any action, if the strategies are already fetched", () => {
const store = createStore({
namespaceStrategies: strategies
});
store.dispatch(fetchNamespaceStrategiesIfNeeded());
expect(store.getActions().length).toBe(0);
});
});
describe("namespace strategies reducer", () => {
it("should return unmodified state on unknown action", () => {
const state = {};
expect(reducer(state)).toBe(state);
});
it("should store the strategies on success", () => {
const newState = reducer({}, fetchNamespaceStrategiesSuccess(strategies));
expect(newState).toBe(strategies);
});
it("should clear store if config was modified", () => {
const modifyConfigAction = {
type: MODIFY_CONFIG_SUCCESS,
payload: {
namespaceStrategy: "CustomNamespaceStrategy"
}
};
const newState = reducer(strategies, modifyConfigAction);
expect(newState.current).toEqual("CustomNamespaceStrategy");
});
});
describe("namespace strategy selectors", () => {
const error = new Error("The end of the universe");
it("should return an empty object", () => {
expect(getNamespaceStrategies({})).toEqual({});
});
it("should return the namespace strategies", () => {
const state = {
namespaceStrategies: strategies
};
expect(getNamespaceStrategies(state)).toBe(strategies);
});
it("should return true, when fetch namespace strategies is pending", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(isFetchNamespaceStrategiesPending(state)).toEqual(true);
});
it("should return false, when fetch strategies is not pending", () => {
expect(isFetchNamespaceStrategiesPending({})).toEqual(false);
});
it("should return error when fetch namespace strategies did fail", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: error
}
};
expect(getFetchNamespaceStrategiesFailure(state)).toEqual(error);
});
it("should return undefined when fetch strategies did not fail", () => {
expect(getFetchNamespaceStrategiesFailure({})).toBe(undefined);
});
});

View File

@@ -5,7 +5,7 @@ import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { withRouter } from "react-router-dom"; import { withRouter } from "react-router-dom";
import { Loading, ErrorPage } from "@scm-manager/ui-components"; import { Loading, ErrorBoundary } from "@scm-manager/ui-components";
import { import {
fetchIndexResources, fetchIndexResources,
getFetchIndexResourcesFailure, getFetchIndexResourcesFailure,
@@ -15,6 +15,7 @@ import {
import PluginLoader from "./PluginLoader"; import PluginLoader from "./PluginLoader";
import type { IndexResources } from "@scm-manager/ui-types"; import type { IndexResources } from "@scm-manager/ui-types";
import ScrollToTop from "./ScrollToTop"; import ScrollToTop from "./ScrollToTop";
import IndexErrorPage from "./IndexErrorPage";
type Props = { type Props = {
error: Error, error: Error,
@@ -55,17 +56,12 @@ class Index extends Component<Props, State> {
const { pluginsLoaded } = this.state; const { pluginsLoaded } = this.state;
if (error) { if (error) {
return ( return <IndexErrorPage error={error}/>;
<ErrorPage
title={t("app.error.title")}
subtitle={t("app.error.subtitle")}
error={error}
/>
);
} else if (loading || !indexResources) { } else if (loading || !indexResources) {
return <Loading />; return <Loading />;
} else { } else {
return ( return (
<ErrorBoundary fallback={IndexErrorPage}>
<ScrollToTop> <ScrollToTop>
<PluginLoader <PluginLoader
loaded={pluginsLoaded} loaded={pluginsLoaded}
@@ -74,6 +70,7 @@ class Index extends Component<Props, State> {
<App /> <App />
</PluginLoader> </PluginLoader>
</ScrollToTop> </ScrollToTop>
</ErrorBoundary>
); );
} }
} }

View File

@@ -0,0 +1,26 @@
//@flow
import React from "react";
import { translate, type TFunction } from "react-i18next";
import { ErrorPage } from "@scm-manager/ui-components";
type Props = {
error: Error,
t: TFunction
}
class IndexErrorPage extends React.Component<Props> {
render() {
const { error, t } = this.props;
return (
<ErrorPage
title={t("app.error.title")}
subtitle={t("app.error.subtitle")}
error={error}
/>
);
}
}
export default translate("commons")(IndexErrorPage);

View File

@@ -9,7 +9,7 @@ import Users from "../users/containers/Users";
import Login from "../containers/Login"; import Login from "../containers/Login";
import Logout from "../containers/Logout"; import Logout from "../containers/Logout";
import { ProtectedRoute } from "@scm-manager/ui-components"; import {ProtectedRoute} from "@scm-manager/ui-components";
import {binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import {binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import AddUser from "../users/containers/AddUser"; import AddUser from "../users/containers/AddUser";

View File

@@ -81,7 +81,14 @@ class PluginLoader extends React.Component<Props, State> {
}; };
loadBundle = (bundle: string) => { loadBundle = (bundle: string) => {
return fetch(bundle) return fetch(bundle, {
credentials: "same-origin",
headers: {
Cache: "no-cache",
// identify the request as ajax request
"X-Requested-With": "XMLHttpRequest"
}
})
.then(response => { .then(response => {
return response.text(); return response.text();
}) })

View File

@@ -15,6 +15,7 @@ import pending from "./modules/pending";
import failure from "./modules/failure"; import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions"; import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config"; import config from "./config/modules/config";
import namespaceStrategies from "./config/modules/namespaceStrategies";
import indexResources from "./modules/indexResource"; import indexResources from "./modules/indexResource";
import type { BrowserHistory } from "history/createBrowserHistory"; import type { BrowserHistory } from "history/createBrowserHistory";
@@ -38,7 +39,8 @@ function createReduxStore(history: BrowserHistory) {
groups, groups,
auth, auth,
config, config,
sources sources,
namespaceStrategies
}); });
return createStore( return createStore(

View File

@@ -13,8 +13,8 @@ class RepositoryDetails extends React.Component<Props> {
const { repository } = this.props; const { repository } = this.props;
return ( return (
<div> <div>
<RepositoryDetailTable repository={repository} /> <RepositoryDetailTable repository={repository}/>
<hr /> <hr/>
<div className="content"> <div className="content">
<ExtensionPoint <ExtensionPoint
name="repos.repository-details.information" name="repos.repository-details.information"

View File

@@ -8,6 +8,7 @@ import {
SubmitButton, SubmitButton,
Textarea Textarea
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Repository, RepositoryType } from "@scm-manager/ui-types"; import type { Repository, RepositoryType } from "@scm-manager/ui-types";
import * as validator from "./repositoryValidation"; import * as validator from "./repositoryValidation";
@@ -15,16 +16,20 @@ type Props = {
submitForm: Repository => void, submitForm: Repository => void,
repository?: Repository, repository?: Repository,
repositoryTypes: RepositoryType[], repositoryTypes: RepositoryType[],
namespaceStrategy: string,
loading?: boolean, loading?: boolean,
t: string => string t: string => string
}; };
type State = { type State = {
repository: Repository, repository: Repository,
namespaceValidationError: boolean,
nameValidationError: boolean, nameValidationError: boolean,
contactValidationError: boolean contactValidationError: boolean
}; };
const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
class RepositoryForm extends React.Component<Props, State> { class RepositoryForm extends React.Component<Props, State> {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
@@ -38,9 +43,9 @@ class RepositoryForm extends React.Component<Props, State> {
description: "", description: "",
_links: {} _links: {}
}, },
namespaceValidationError: false,
nameValidationError: false, nameValidationError: false,
contactValidationError: false, contactValidationError: false
descriptionValidationError: false
}; };
} }
@@ -59,11 +64,15 @@ class RepositoryForm extends React.Component<Props, State> {
} }
isValid = () => { isValid = () => {
const repository = this.state.repository; const { namespaceStrategy } = this.props;
const { repository } = this.state;
return !( return !(
this.state.namespaceValidationError ||
this.state.nameValidationError || this.state.nameValidationError ||
this.state.contactValidationError || this.state.contactValidationError ||
this.isFalsy(repository.name) this.isFalsy(repository.name) ||
(namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY &&
this.isFalsy(repository.namespace))
); );
}; };
@@ -78,10 +87,24 @@ class RepositoryForm extends React.Component<Props, State> {
return !this.props.repository; return !this.props.repository;
}; };
isModifiable = () => {
return !!this.props.repository && !!this.props.repository._links.update;
};
render() { render() {
const { loading, t } = this.props; const { loading, t } = this.props;
const repository = this.state.repository; const repository = this.state.repository;
const disabled = !this.isModifiable() && !this.isCreateMode();
const submitButton = disabled ? null : (
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("repositoryForm.submit")}
/>
);
let subtitle = null; let subtitle = null;
if (this.props.repository) { if (this.props.repository) {
// edit existing repo // edit existing repo
@@ -100,6 +123,7 @@ class RepositoryForm extends React.Component<Props, State> {
validationError={this.state.contactValidationError} validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")} errorMessage={t("validation.contact-invalid")}
helpText={t("help.contactHelpText")} helpText={t("help.contactHelpText")}
disabled={disabled}
/> />
<Textarea <Textarea
@@ -107,12 +131,9 @@ class RepositoryForm extends React.Component<Props, State> {
onChange={this.handleDescriptionChange} onChange={this.handleDescriptionChange}
value={repository ? repository.description : ""} value={repository ? repository.description : ""}
helpText={t("help.descriptionHelpText")} helpText={t("help.descriptionHelpText")}
disabled={disabled}
/> />
<SubmitButton {submitButton}
disabled={!this.isValid()}
loading={loading}
label={t("repositoryForm.submit")}
/>
</form> </form>
</> </>
); );
@@ -127,6 +148,31 @@ class RepositoryForm extends React.Component<Props, State> {
}); });
} }
renderNamespaceField = () => {
const { namespaceStrategy, t } = this.props;
const repository = this.state.repository;
const props = {
label: t("repository.namespace"),
helpText: t("help.namespaceHelpText"),
value: repository ? repository.namespace : "",
onChange: this.handleNamespaceChange,
errorMessage: t("validation.namespace-invalid"),
validationError: this.state.namespaceValidationError
};
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
return <InputField {...props} />;
}
return (
<ExtensionPoint
name="repos.create.namespace"
props={props}
renderAll={false}
/>
);
};
renderCreateOnlyFields() { renderCreateOnlyFields() {
if (!this.isCreateMode()) { if (!this.isCreateMode()) {
return null; return null;
@@ -135,6 +181,7 @@ class RepositoryForm extends React.Component<Props, State> {
const repository = this.state.repository; const repository = this.state.repository;
return ( return (
<> <>
{this.renderNamespaceField()}
<InputField <InputField
label={t("repository.name")} label={t("repository.name")}
onChange={this.handleNameChange} onChange={this.handleNameChange}
@@ -154,6 +201,13 @@ class RepositoryForm extends React.Component<Props, State> {
); );
} }
handleNamespaceChange = (namespace: string) => {
this.setState({
namespaceValidationError: !validator.isNameValid(namespace),
repository: { ...this.state.repository, namespace }
});
};
handleNameChange = (name: string) => { handleNameChange = (name: string) => {
this.setState({ this.setState({
nameValidationError: !validator.isNameValid(name), nameValidationError: !validator.isNameValid(name),

View File

@@ -1,8 +1,10 @@
// @flow // @flow
import { validation } from "@scm-manager/ui-components"; import { validation } from "@scm-manager/ui-components";
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
export const isNameValid = (name: string) => { export const isNameValid = (name: string) => {
return validation.isNameValid(name); return nameRegex.test(name);
}; };
export function isContactValid(mail: string) { export function isContactValid(mail: string) {

View File

@@ -11,6 +11,81 @@ describe("repository name validation", () => {
expect(validator.isNameValid("scm/manager")).toBe(false); expect(validator.isNameValid("scm/manager")).toBe(false);
expect(validator.isNameValid("scm/ma/nager")).toBe(false); expect(validator.isNameValid("scm/ma/nager")).toBe(false);
}); });
it("should allow same names as the backend", () => {
const validPaths = [
"scm",
"s",
"sc",
".hiddenrepo",
"b.",
"...",
"..c",
"d..",
"a..c"
];
validPaths.forEach((path) =>
expect(validator.isNameValid(path)).toBe(true)
);
});
it("should deny same names as the backend", () => {
const invalidPaths = [
".",
"/",
"//",
"..",
"/.",
"/..",
"./",
"../",
"/../",
"/./",
"/...",
"/abc",
".../",
"/sdf/",
"asdf/",
"./b",
"scm/plugins/.",
"scm/../plugins",
"scm/main/",
"/scm/main/",
"scm/./main",
"scm//main",
"scm\\main",
"scm/main-$HOME",
"scm/main-${HOME}-home",
"scm/main-%HOME-home",
"scm/main-%HOME%-home",
"abc$abc",
"abc%abc",
"abc<abc",
"abc>abc",
"abc#abc",
"abc+abc",
"abc{abc",
"abc}abc",
"abc(abc",
"abc)abc",
"abc[abc",
"abc]abc",
"abc|abc",
"scm/main",
"scm/plugins/git-plugin",
".scm/plugins",
"a/b..",
"a/..b",
"scm/main",
"scm/plugins/git-plugin",
"scm/plugins/git-plugin"
];
invalidPaths.forEach((path) =>
expect(validator.isNameValid(path)).toBe(false)
);
});
}); });
describe("repository contact validation", () => { describe("repository contact validation", () => {

View File

@@ -4,7 +4,7 @@ import { connect } from "react-redux";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { Page } from "@scm-manager/ui-components"; import { Page } from "@scm-manager/ui-components";
import RepositoryForm from "../components/form"; import RepositoryForm from "../components/form";
import type { Repository, RepositoryType } from "@scm-manager/ui-types"; import type { Repository, RepositoryType, NamespaceStrategies } from "@scm-manager/ui-types";
import { import {
fetchRepositoryTypesIfNeeded, fetchRepositoryTypesIfNeeded,
getFetchRepositoryTypesFailure, getFetchRepositoryTypesFailure,
@@ -19,15 +19,21 @@ import {
} from "../modules/repos"; } from "../modules/repos";
import type { History } from "history"; import type { History } from "history";
import { getRepositoriesLink } from "../../modules/indexResource"; import { getRepositoriesLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
getFetchNamespaceStrategiesFailure, getNamespaceStrategies, isFetchNamespaceStrategiesPending
} from "../../config/modules/namespaceStrategies";
type Props = { type Props = {
repositoryTypes: RepositoryType[], repositoryTypes: RepositoryType[],
typesLoading: boolean, namespaceStrategies: NamespaceStrategies,
pageLoading: boolean,
createLoading: boolean, createLoading: boolean,
error: Error, error: Error,
repoLink: string, repoLink: string,
// dispatch functions // dispatch functions
fetchNamespaceStrategiesIfNeeded: () => void,
fetchRepositoryTypesIfNeeded: () => void, fetchRepositoryTypesIfNeeded: () => void,
createRepo: ( createRepo: (
link: string, link: string,
@@ -45,6 +51,7 @@ class Create extends React.Component<Props> {
componentDidMount() { componentDidMount() {
this.props.resetForm(); this.props.resetForm();
this.props.fetchRepositoryTypesIfNeeded(); this.props.fetchRepositoryTypesIfNeeded();
this.props.fetchNamespaceStrategiesIfNeeded();
} }
repoCreated = (repo: Repository) => { repoCreated = (repo: Repository) => {
@@ -55,9 +62,10 @@ class Create extends React.Component<Props> {
render() { render() {
const { const {
typesLoading, pageLoading,
createLoading, createLoading,
repositoryTypes, repositoryTypes,
namespaceStrategies,
createRepo, createRepo,
error error
} = this.props; } = this.props;
@@ -67,13 +75,14 @@ class Create extends React.Component<Props> {
<Page <Page
title={t("create.title")} title={t("create.title")}
subtitle={t("create.subtitle")} subtitle={t("create.subtitle")}
loading={typesLoading} loading={pageLoading}
error={error} error={error}
showContentOnError={true} showContentOnError={true}
> >
<RepositoryForm <RepositoryForm
repositoryTypes={repositoryTypes} repositoryTypes={repositoryTypes}
loading={createLoading} loading={createLoading}
namespaceStrategy={namespaceStrategies.current}
submitForm={repo => { submitForm={repo => {
createRepo(repoLink, repo, (repo: Repository) => createRepo(repoLink, repo, (repo: Repository) =>
this.repoCreated(repo) this.repoCreated(repo)
@@ -87,14 +96,18 @@ class Create extends React.Component<Props> {
const mapStateToProps = state => { const mapStateToProps = state => {
const repositoryTypes = getRepositoryTypes(state); const repositoryTypes = getRepositoryTypes(state);
const typesLoading = isFetchRepositoryTypesPending(state); const namespaceStrategies = getNamespaceStrategies(state);
const pageLoading = isFetchRepositoryTypesPending(state)
|| isFetchNamespaceStrategiesPending(state);
const createLoading = isCreateRepoPending(state); const createLoading = isCreateRepoPending(state);
const error = const error = getFetchRepositoryTypesFailure(state)
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state); || getCreateRepoFailure(state)
|| getFetchNamespaceStrategiesFailure(state);
const repoLink = getRepositoriesLink(state); const repoLink = getRepositoriesLink(state);
return { return {
repositoryTypes, repositoryTypes,
typesLoading, namespaceStrategies,
pageLoading,
createLoading, createLoading,
error, error,
repoLink repoLink
@@ -106,6 +119,9 @@ const mapDispatchToProps = dispatch => {
fetchRepositoryTypesIfNeeded: () => { fetchRepositoryTypesIfNeeded: () => {
dispatch(fetchRepositoryTypesIfNeeded()); dispatch(fetchRepositoryTypesIfNeeded());
}, },
fetchNamespaceStrategiesIfNeeded: () => {
dispatch(fetchNamespaceStrategiesIfNeeded());
},
createRepo: ( createRepo: (
link: string, link: string,
repository: Repository, repository: Repository,

View File

@@ -1,11 +1,9 @@
// @flow // @flow
import React from "react"; import React from "react";
import { translate } from "react-i18next"; import { translate } from "react-i18next";
import { apiClient } from "@scm-manager/ui-components"; import { apiClient, SyntaxHighlighter } from "@scm-manager/ui-components";
import type { File } from "@scm-manager/ui-types"; import type { File } from "@scm-manager/ui-types";
import { ErrorNotification, Loading } from "@scm-manager/ui-components"; import { ErrorNotification, Loading } from "@scm-manager/ui-components";
import SyntaxHighlighter from "react-syntax-highlighter";
import { arduinoLight } from "react-syntax-highlighter/styles/hljs";
type Props = { type Props = {
t: string => string, t: string => string,
@@ -68,12 +66,9 @@ class SourcecodeViewer extends React.Component<Props, State> {
return ( return (
<SyntaxHighlighter <SyntaxHighlighter
showLineNumbers="true"
language={getLanguage(language)} language={getLanguage(language)}
style={arduinoLight} value= {content}
> />
{content}
</SyntaxHighlighter>
); );
} }
} }

View File

@@ -31,7 +31,7 @@ public class ConfigDto extends HalRepresentation {
private String pluginUrl; private String pluginUrl;
private long loginAttemptLimitTimeout; private long loginAttemptLimitTimeout;
private boolean enabledXsrfProtection; private boolean enabledXsrfProtection;
private String defaultNamespaceStrategy; private String namespaceStrategy;
@Override @Override
@SuppressWarnings("squid:S1185") // We want to have this method available in this package @SuppressWarnings("squid:S1185") // We want to have this method available in this package

View File

@@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import com.webcohesion.enunciate.metadata.rs.TypeHint; import com.webcohesion.enunciate.metadata.rs.TypeHint;
import sonia.scm.config.ConfigurationPermissions; import sonia.scm.config.ConfigurationPermissions;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.NamespaceStrategyValidator;
import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.util.ScmConfigurationUtil;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
@@ -27,12 +28,16 @@ public class ConfigResource {
private final ConfigDtoToScmConfigurationMapper dtoToConfigMapper; private final ConfigDtoToScmConfigurationMapper dtoToConfigMapper;
private final ScmConfigurationToConfigDtoMapper configToDtoMapper; private final ScmConfigurationToConfigDtoMapper configToDtoMapper;
private final ScmConfiguration configuration; private final ScmConfiguration configuration;
private final NamespaceStrategyValidator namespaceStrategyValidator;
@Inject @Inject
public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper, ScmConfigurationToConfigDtoMapper configToDtoMapper, ScmConfiguration configuration) { public ConfigResource(ConfigDtoToScmConfigurationMapper dtoToConfigMapper,
ScmConfigurationToConfigDtoMapper configToDtoMapper,
ScmConfiguration configuration, NamespaceStrategyValidator namespaceStrategyValidator) {
this.dtoToConfigMapper = dtoToConfigMapper; this.dtoToConfigMapper = dtoToConfigMapper;
this.configToDtoMapper = configToDtoMapper; this.configToDtoMapper = configToDtoMapper;
this.configuration = configuration; this.configuration = configuration;
this.namespaceStrategyValidator = namespaceStrategyValidator;
} }
/** /**
@@ -78,6 +83,9 @@ public class ConfigResource {
// But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later. // But to where to check? load() or store()? Leave it for now, SCMv1 legacy that can be cleaned up later.
ConfigurationPermissions.write(configuration).check(); ConfigurationPermissions.write(configuration).check();
// ensure the namespace strategy is valid
namespaceStrategyValidator.check(configDto.getNamespaceStrategy());
ScmConfiguration config = dtoToConfigMapper.map(configDto); ScmConfiguration config = dtoToConfigMapper.map(configDto);
synchronized (ScmConfiguration.class) { synchronized (ScmConfiguration.class) {
configuration.load(config); configuration.load(config);

View File

@@ -59,6 +59,9 @@ public class IndexDtoGenerator extends HalAppenderMapper {
builder.single(link("permissions", resourceLinks.permissions().self())); builder.single(link("permissions", resourceLinks.permissions().self()));
} }
builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self())); builder.single(link("availableRepositoryPermissions", resourceLinks.availableRepositoryPermissions().self()));
builder.single(link("repositoryTypes", resourceLinks.repositoryTypeCollection().self()));
builder.single(link("namespaceStrategies", resourceLinks.namespaceStrategies().self()));
} else { } else {
builder.single(link("login", resourceLinks.authentication().jsonLogin())); builder.single(link("login", resourceLinks.authentication().jsonLogin()));
} }

View File

@@ -0,0 +1,20 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links;
import lombok.Getter;
import java.util.List;
@Getter
public class NamespaceStrategiesDto extends HalRepresentation {
private String current;
private List<String> available;
public NamespaceStrategiesDto(String current, List<String> available, Links links) {
super(links);
this.current = current;
this.available = available;
}
}

View File

@@ -0,0 +1,63 @@
package sonia.scm.api.v2.resources;
import de.otto.edison.hal.Links;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.UriInfo;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* RESTFul WebService Endpoint for namespace strategies.
*/
@Path(NamespaceStrategyResource.PATH)
public class NamespaceStrategyResource {
static final String PATH = "v2/namespaceStrategies";
private Set<NamespaceStrategy> namespaceStrategies;
private Provider<NamespaceStrategy> namespaceStrategyProvider;
@Inject
public NamespaceStrategyResource(Set<NamespaceStrategy> namespaceStrategies, Provider<NamespaceStrategy> namespaceStrategyProvider) {
this.namespaceStrategies = namespaceStrategies;
this.namespaceStrategyProvider = namespaceStrategyProvider;
}
/**
* Returns all available namespace strategies and the current selected.
*
* @param uriInfo uri info
*
* @return available and current namespace strategies
*/
@GET
@Path("")
@Produces(VndMediaType.NAMESPACE_STRATEGIES)
public NamespaceStrategiesDto get(@Context UriInfo uriInfo) {
String currentStrategy = strategyAsString(namespaceStrategyProvider.get());
List<String> availableStrategies = collectStrategyNames();
return new NamespaceStrategiesDto(currentStrategy, availableStrategies, createLinks(uriInfo));
}
private Links createLinks(@Context UriInfo uriInfo) {
return Links.linkingTo().self(uriInfo.getAbsolutePath().toASCIIString()).build();
}
private String strategyAsString(NamespaceStrategy namespaceStrategy) {
return namespaceStrategy.getClass().getSimpleName();
}
private List<String> collectStrategyNames() {
return namespaceStrategies.stream().map(this::strategyAsString).collect(Collectors.toList());
}
}

View File

@@ -9,6 +9,7 @@ import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotEmpty; import org.hibernate.validator.constraints.NotEmpty;
import sonia.scm.util.ValidationUtil;
import javax.validation.constraints.Pattern; import javax.validation.constraints.Pattern;
import java.time.Instant; import java.time.Instant;
@@ -25,8 +26,9 @@ public class RepositoryDto extends HalRepresentation {
private List<HealthCheckFailureDto> healthCheckFailures; private List<HealthCheckFailureDto> healthCheckFailures;
@JsonInclude(JsonInclude.Include.NON_NULL) @JsonInclude(JsonInclude.Include.NON_NULL)
private Instant lastModified; private Instant lastModified;
// we could not validate the namespace, this must be done by the namespace strategy
private String namespace; private String namespace;
@Pattern(regexp = "^[A-z0-9\\-_]+$") @Pattern(regexp = ValidationUtil.REGEX_REPOSITORYNAME)
private String name; private String name;
private boolean archived = false; private boolean archived = false;
@NotEmpty @NotEmpty

View File

@@ -277,6 +277,23 @@ class ResourceLinks {
} }
} }
public NamespaceStrategiesLinks namespaceStrategies() {
return new NamespaceStrategiesLinks(scmPathInfoStore.get());
}
static class NamespaceStrategiesLinks {
private final LinkBuilder namespaceStrategiesLinkBuilder;
NamespaceStrategiesLinks(ScmPathInfo pathInfo) {
namespaceStrategiesLinkBuilder = new LinkBuilder(pathInfo, NamespaceStrategyResource.class);
}
String self() {
return namespaceStrategiesLinkBuilder.method("get").parameters().href();
}
}
public RepositoryTypeLinks repositoryType() { public RepositoryTypeLinks repositoryType() {
return new RepositoryTypeLinks(scmPathInfoStore.get()); return new RepositoryTypeLinks(scmPathInfoStore.get());
} }

View File

@@ -0,0 +1,29 @@
package sonia.scm.repository;
import com.google.common.annotations.VisibleForTesting;
import sonia.scm.plugin.Extension;
import javax.inject.Inject;
import java.time.Clock;
import java.time.Year;
@Extension
public class CurrentYearNamespaceStrategy implements NamespaceStrategy {
private final Clock clock;
@Inject
public CurrentYearNamespaceStrategy() {
this(Clock.systemDefaultZone());
}
@VisibleForTesting
CurrentYearNamespaceStrategy(Clock clock) {
this.clock = clock;
}
@Override
public String createNamespace(Repository repository) {
return String.valueOf(Year.now(clock).getValue());
}
}

View File

@@ -0,0 +1,20 @@
package sonia.scm.repository;
import sonia.scm.plugin.Extension;
import sonia.scm.util.ValidationUtil;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
@Extension
public class CustomNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
String namespace = repository.getNamespace();
doThrow()
.violation("invalid namespace", "namespace")
.when(!ValidationUtil.isRepositoryNameValid(namespace));
return namespace;
}
}

View File

@@ -1,24 +0,0 @@
package sonia.scm.repository;
import com.google.common.base.Strings;
import org.apache.shiro.SecurityUtils;
import sonia.scm.plugin.Extension;
/**
* The DefaultNamespaceStrategy returns the predefined namespace of the given repository, if the namespace was not set
* the username of the currently loggedin user is used.
*
* @since 2.0.0
*/
@Extension
public class DefaultNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
String namespace = repository.getNamespace();
if (Strings.isNullOrEmpty(namespace)) {
namespace = SecurityUtils.getSubject().getPrincipal().toString();
}
return namespace;
}
}

View File

@@ -52,6 +52,7 @@ import sonia.scm.util.CollectionAppender;
import sonia.scm.util.IOUtil; import sonia.scm.util.IOUtil;
import sonia.scm.util.Util; import sonia.scm.util.Util;
import javax.inject.Provider;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
@@ -85,7 +86,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
private final KeyGenerator keyGenerator; private final KeyGenerator keyGenerator;
private final RepositoryDAO repositoryDAO; private final RepositoryDAO repositoryDAO;
private final Set<Type> types; private final Set<Type> types;
private NamespaceStrategy namespaceStrategy; private final Provider<NamespaceStrategy> namespaceStrategyProvider;
private final ManagerDaoAdapter<Repository> managerDaoAdapter; private final ManagerDaoAdapter<Repository> managerDaoAdapter;
@@ -93,11 +94,11 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
public DefaultRepositoryManager(ScmConfiguration configuration, public DefaultRepositoryManager(ScmConfiguration configuration,
SCMContextProvider contextProvider, KeyGenerator keyGenerator, SCMContextProvider contextProvider, KeyGenerator keyGenerator,
RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet, RepositoryDAO repositoryDAO, Set<RepositoryHandler> handlerSet,
NamespaceStrategy namespaceStrategy) { Provider<NamespaceStrategy> namespaceStrategyProvider) {
this.configuration = configuration; this.configuration = configuration;
this.keyGenerator = keyGenerator; this.keyGenerator = keyGenerator;
this.repositoryDAO = repositoryDAO; this.repositoryDAO = repositoryDAO;
this.namespaceStrategy = namespaceStrategy; this.namespaceStrategyProvider = namespaceStrategyProvider;
ThreadFactory factory = new ThreadFactoryBuilder() ThreadFactory factory = new ThreadFactoryBuilder()
.setNameFormat(THREAD_NAME).build(); .setNameFormat(THREAD_NAME).build();
@@ -131,7 +132,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager {
public Repository create(Repository repository, boolean initRepository) { public Repository create(Repository repository, boolean initRepository) {
repository.setId(keyGenerator.createKey()); repository.setId(keyGenerator.createKey());
repository.setNamespace(namespaceStrategy.createNamespace(repository)); repository.setNamespace(namespaceStrategyProvider.get().createNamespace(repository));
logger.info("create repository {}/{} of type {} in namespace {}", repository.getNamespace(), repository.getName(), repository.getType(), repository.getNamespace()); logger.info("create repository {}/{} of type {} in namespace {}", repository.getNamespace(), repository.getName(), repository.getType(), repository.getNamespace());

View File

@@ -1,5 +1,7 @@
package sonia.scm.repository; package sonia.scm.repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import javax.inject.Inject; import javax.inject.Inject;
@@ -8,6 +10,8 @@ import java.util.Set;
public class NamespaceStrategyProvider implements Provider<NamespaceStrategy> { public class NamespaceStrategyProvider implements Provider<NamespaceStrategy> {
private static final Logger LOG = LoggerFactory.getLogger(NamespaceStrategyProvider.class);
private final Set<NamespaceStrategy> strategies; private final Set<NamespaceStrategy> strategies;
private final ScmConfiguration scmConfiguration; private final ScmConfiguration scmConfiguration;
@@ -19,14 +23,16 @@ public class NamespaceStrategyProvider implements Provider<NamespaceStrategy> {
@Override @Override
public NamespaceStrategy get() { public NamespaceStrategy get() {
String namespaceStrategy = scmConfiguration.getDefaultNamespaceStrategy(); String namespaceStrategy = scmConfiguration.getNamespaceStrategy();
for (NamespaceStrategy s : this.strategies) { for (NamespaceStrategy s : this.strategies) {
if (s.getClass().getCanonicalName().equals(namespaceStrategy)) { if (s.getClass().getSimpleName().equals(namespaceStrategy)) {
return s; return s;
} }
} }
return null;
LOG.warn("could not find namespace strategy {}, using default strategy", namespaceStrategy);
return new UsernameNamespaceStrategy();
} }
} }

View File

@@ -0,0 +1,26 @@
package sonia.scm.repository;
import javax.inject.Inject;
import java.util.Set;
import static sonia.scm.ScmConstraintViolationException.Builder.doThrow;
public class NamespaceStrategyValidator {
private final Set<NamespaceStrategy> strategies;
@Inject
public NamespaceStrategyValidator(Set<NamespaceStrategy> strategies) {
this.strategies = strategies;
}
public void check(String name) {
doThrow()
.violation("unknown NamespaceStrategy " + name, "namespaceStrategy")
.when(!isValid(name));
}
private boolean isValid(String name) {
return strategies.stream().anyMatch(ns -> ns.getClass().getSimpleName().equals(name));
}
}

View File

@@ -0,0 +1,11 @@
package sonia.scm.repository;
import sonia.scm.plugin.Extension;
@Extension
public class RepositoryTypeNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return repository.getType();
}
}

View File

@@ -0,0 +1,13 @@
package sonia.scm.repository;
import org.apache.shiro.SecurityUtils;
import sonia.scm.plugin.Extension;
@Extension
public class UsernameNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return SecurityUtils.getSubject().getPrincipal().toString();
}
}

View File

@@ -36,6 +36,11 @@ import com.google.common.collect.Maps;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.ExternalGroupNames;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
@@ -46,11 +51,6 @@ import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.group.GroupNames;
/** /**
* Jwt implementation of {@link AccessTokenBuilder}. * Jwt implementation of {@link AccessTokenBuilder}.
@@ -210,9 +210,9 @@ public final class JwtAccessTokenBuilder implements AccessTokenBuilder {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups); claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groups);
} else { } else {
Subject currentSubject = SecurityUtils.getSubject(); Subject currentSubject = SecurityUtils.getSubject();
GroupNames groupNames = currentSubject.getPrincipals().oneByType(GroupNames.class); ExternalGroupNames externalGroupNames = currentSubject.getPrincipals().oneByType(ExternalGroupNames.class);
if (groupNames != null && groupNames.isExternal()) { if (externalGroupNames != null) {
claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, groupNames.getCollection().toArray(new String[]{})); claims.put(JwtAccessToken.GROUPS_CLAIM_KEY, externalGroupNames.getCollection().toArray(new String[]{}));
} }
} }

View File

@@ -148,5 +148,11 @@
"displayName": "Ungültige Eingabe", "displayName": "Ungültige Eingabe",
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut." "description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
} }
},
"namespaceStrategies": {
"UsernameNamespaceStrategy": "Benutzername",
"CustomNamespaceStrategy": "Benutzerdefiniert",
"CurrentYearNamespaceStrategy": "Aktuelles Jahr",
"RepositoryTypeNamespaceStrategy": "Repository Typ"
} }
} }

View File

@@ -148,5 +148,11 @@
"displayName": "Illegal input", "displayName": "Illegal input",
"description": "The values could not be validated. Please correct your input and try again." "description": "The values could not be validated. Please correct your input and try again."
} }
},
"namespaceStrategies": {
"UsernameNamespaceStrategy": "Username",
"CustomNamespaceStrategy": "Custom",
"CurrentYearNamespaceStrategy": "Current year",
"RepositoryTypeNamespaceStrategy": "Repository type"
} }
} }

View File

@@ -49,7 +49,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
assertEquals("https://plug.ins" , config.getPluginUrl()); assertEquals("https://plug.ins" , config.getPluginUrl());
assertEquals(40 , config.getLoginAttemptLimitTimeout()); assertEquals(40 , config.getLoginAttemptLimitTimeout());
assertTrue(config.isEnabledXsrfProtection()); assertTrue(config.isEnabledXsrfProtection());
assertEquals("username", config.getDefaultNamespaceStrategy()); assertEquals("username", config.getNamespaceStrategy());
} }
private ConfigDto createDefaultDto() { private ConfigDto createDefaultDto() {
@@ -72,7 +72,7 @@ public class ConfigDtoToScmConfigurationMapperTest {
configDto.setPluginUrl("https://plug.ins"); configDto.setPluginUrl("https://plug.ins");
configDto.setLoginAttemptLimitTimeout(40); configDto.setLoginAttemptLimitTimeout(40);
configDto.setEnabledXsrfProtection(true); configDto.setEnabledXsrfProtection(true);
configDto.setDefaultNamespaceStrategy("username"); configDto.setNamespaceStrategy("username");
return configDto; return configDto;
} }

View File

@@ -13,7 +13,9 @@ import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException; import org.junit.rules.ExpectedException;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.repository.NamespaceStrategyValidator;
import sonia.scm.web.VndMediaType; import sonia.scm.web.VndMediaType;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@@ -22,10 +24,12 @@ import java.io.UnsupportedEncodingException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.MockitoAnnotations.initMocks; import static org.mockito.MockitoAnnotations.initMocks;
@SubjectAware( @SubjectAware(
@@ -46,6 +50,9 @@ public class ConfigResourceTest {
@SuppressWarnings("unused") // Is injected @SuppressWarnings("unused") // Is injected
private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); private ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri);
@Mock
private NamespaceStrategyValidator namespaceStrategyValidator;
@InjectMocks @InjectMocks
private ConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper; private ConfigDtoToScmConfigurationMapperImpl dtoToConfigMapper;
@InjectMocks @InjectMocks
@@ -62,7 +69,7 @@ public class ConfigResourceTest {
public void prepareEnvironment() { public void prepareEnvironment() {
initMocks(this); initMocks(this);
ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration()); ConfigResource configResource = new ConfigResource(dtoToConfigMapper, configToDtoMapper, createConfiguration(), namespaceStrategyValidator);
dispatcher.getRegistry().addSingletonResource(configResource); dispatcher.getRegistry().addSingletonResource(configResource);
} }
@@ -119,6 +126,21 @@ public class ConfigResourceTest {
} }
@Test
@SubjectAware(username = "readWrite")
public void shouldValidateNamespaceStrategy() throws URISyntaxException {
MockHttpRequest request = MockHttpRequest.put("/" + ConfigResource.CONFIG_PATH_V2)
.contentType(VndMediaType.CONFIG)
.content("{ \"namespaceStrategy\": \"AwesomeStrategy\" }".getBytes(StandardCharsets.UTF_8));
MockHttpResponse response = new MockHttpResponse();
dispatcher.invoke(request, response);
assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus());
verify(namespaceStrategyValidator).check("AwesomeStrategy");
}
private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException { private MockHttpRequest post(String resourceName) throws IOException, URISyntaxException {
URL url = Resources.getResource(resourceName); URL url = Resources.getResource(resourceName);
byte[] configJson = Resources.toByteArray(url); byte[] configJson = Resources.toByteArray(url);

View File

@@ -0,0 +1,74 @@
package sonia.scm.api.v2.resources;
import com.google.common.collect.Lists;
import com.google.inject.util.Providers;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import sonia.scm.repository.NamespaceStrategy;
import sonia.scm.repository.Repository;
import javax.inject.Provider;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class NamespaceStrategyResourceTest {
@Mock
private UriInfo uriInfo;
@Test
void shouldReturnNamespaceStrategies() {
when(uriInfo.getAbsolutePath()).thenReturn(URI.create("/namespace-strategies"));
Set<NamespaceStrategy> namespaceStrategies = allStrategies();
Provider<NamespaceStrategy> current = Providers.of(new MegaNamespaceStrategy());
NamespaceStrategyResource resource = new NamespaceStrategyResource(namespaceStrategies, current);
NamespaceStrategiesDto dto = resource.get(uriInfo);
assertThat(dto.getCurrent()).isEqualTo(MegaNamespaceStrategy.class.getSimpleName());
assertThat(dto.getAvailable()).contains(
AwesomeNamespaceStrategy.class.getSimpleName(),
SuperNamespaceStrategy.class.getSimpleName(),
MegaNamespaceStrategy.class.getSimpleName()
);
assertThat(dto.getLinks().getLinkBy("self").get().getHref()).isEqualTo("/namespace-strategies");
}
private Set<NamespaceStrategy> allStrategies() {
return strategies(new AwesomeNamespaceStrategy(), new SuperNamespaceStrategy(), new MegaNamespaceStrategy());
}
private Set<NamespaceStrategy> strategies(NamespaceStrategy... strategies) {
return new LinkedHashSet<>(Lists.newArrayList(strategies));
}
private static class AwesomeNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return "awesome";
}
}
private static class SuperNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return "super";
}
}
private static class MegaNamespaceStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return "mega";
}
}
}

View File

@@ -43,6 +43,8 @@ public class ResourceLinksMock {
when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo)); when(resourceLinks.merge()).thenReturn(new ResourceLinks.MergeLinks(uriInfo));
when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo)); when(resourceLinks.permissions()).thenReturn(new ResourceLinks.PermissionsLinks(uriInfo));
when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo)); when(resourceLinks.availableRepositoryPermissions()).thenReturn(new ResourceLinks.AvailableRepositoryPermissionLinks(uriInfo));
when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo));
when(resourceLinks.namespaceStrategies()).thenReturn(new ResourceLinks.NamespaceStrategiesLinks(uriInfo));
return resourceLinks; return resourceLinks;
} }

View File

@@ -79,7 +79,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
assertEquals("pluginurl" , dto.getPluginUrl()); assertEquals("pluginurl" , dto.getPluginUrl());
assertEquals(2 , dto.getLoginAttemptLimitTimeout()); assertEquals(2 , dto.getLoginAttemptLimitTimeout());
assertTrue(dto.isEnabledXsrfProtection()); assertTrue(dto.isEnabledXsrfProtection());
assertEquals("username", dto.getDefaultNamespaceStrategy()); assertEquals("username", dto.getNamespaceStrategy());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("self").get().getHref());
assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref()); assertEquals(expectedBaseUri.toString(), dto.getLinks().getLinkBy("update").get().getHref());
@@ -117,7 +117,7 @@ public class ScmConfigurationToConfigDtoMapperTest {
config.setPluginUrl("pluginurl"); config.setPluginUrl("pluginurl");
config.setLoginAttemptLimitTimeout(2); config.setLoginAttemptLimitTimeout(2);
config.setEnabledXsrfProtection(true); config.setEnabledXsrfProtection(true);
config.setDefaultNamespaceStrategy("username"); config.setNamespaceStrategy("username");
return config; return config;
} }

View File

@@ -0,0 +1,39 @@
package sonia.scm.repository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.Clock;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CurrentYearNamespaceStrategyTest {
@Mock
private Clock clock;
private NamespaceStrategy namespaceStrategy;
@BeforeEach
void setupObjectUnderTest() {
namespaceStrategy = new CurrentYearNamespaceStrategy(clock);
}
@Test
void shouldReturn1985() {
LocalDateTime dateTime = LocalDateTime.of(1985, 4, 9, 21, 42);
when(clock.instant()).thenReturn(dateTime.toInstant(ZoneOffset.UTC));
when(clock.getZone()).thenReturn(ZoneId.systemDefault());
String namespace = namespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold());
assertThat(namespace).isEqualTo("1985");
}
}

View File

@@ -0,0 +1,28 @@
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import sonia.scm.ScmConstraintViolationException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CustomNamespaceStrategyTest {
private final NamespaceStrategy namespaceStrategy = new CustomNamespaceStrategy();
@Test
void shouldReturnNamespaceFromRepository() {
Repository heartOfGold = RepositoryTestData.createHeartOfGold();
assertThat(namespaceStrategy.createNamespace(heartOfGold)).isEqualTo(RepositoryTestData.NAMESPACE);
}
@Test
void shouldThrowAnValidationExceptionForAnInvalidNamespace() {
Repository repository = new Repository();
repository.setNamespace("..");
repository.setName(".");
assertThrows(ScmConstraintViolationException.class, () -> namespaceStrategy.createNamespace(repository));
}
}

View File

@@ -1,32 +0,0 @@
package sonia.scm.repository;
import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware;
import org.junit.Rule;
import org.junit.Test;
import static org.junit.Assert.*;
@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini")
public class DefaultNamespaceStrategyTest {
@Rule
public ShiroRule shiroRule = new ShiroRule();
private DefaultNamespaceStrategy namespaceStrategy = new DefaultNamespaceStrategy();
@Test
@SubjectAware(username = "trillian", password = "secret")
public void testNamespaceStrategyWithoutPreset() {
assertEquals("trillian", namespaceStrategy.createNamespace(new Repository()));
}
@Test
@SubjectAware(username = "trillian", password = "secret")
public void testNamespaceStrategyWithPreset() {
Repository repository = new Repository();
repository.setNamespace("awesome");
assertEquals("awesome", namespaceStrategy.createNamespace(repository));
}
}

View File

@@ -34,6 +34,7 @@ import com.google.common.base.Stopwatch;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.inject.Provider; import com.google.inject.Provider;
import com.google.inject.util.Providers;
import org.apache.shiro.SecurityUtils; import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationInfo;
@@ -118,7 +119,7 @@ public class DefaultRepositoryManagerPerfTest {
keyGenerator, keyGenerator,
repositoryDAO, repositoryDAO,
handlerSet, handlerSet,
namespaceStrategy Providers.of(namespaceStrategy)
); );
setUpTestRepositories(); setUpTestRepositories();

View File

@@ -37,6 +37,7 @@ import com.github.sdorra.shiro.ShiroRule;
import com.github.sdorra.shiro.SubjectAware; import com.github.sdorra.shiro.SubjectAware;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.inject.util.Providers;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
import org.junit.Before; import org.junit.Before;
@@ -436,7 +437,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase<Repository> {
when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace);
return new DefaultRepositoryManager(configuration, contextProvider, return new DefaultRepositoryManager(configuration, contextProvider,
keyGenerator, repositoryDAO, handlerSet, namespaceStrategy); keyGenerator, repositoryDAO, handlerSet, Providers.of(namespaceStrategy));
} }
private void createRepository(RepositoryManager m, Repository repository) { private void createRepository(RepositoryManager m, Repository repository) {

View File

@@ -0,0 +1,69 @@
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import sonia.scm.config.ScmConfiguration;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class NamespaceStrategyProviderTest {
@Test
void shouldReturnConfiguredStrategy() {
Set<NamespaceStrategy> strategies = allStrategiesAsSet();
ScmConfiguration configuration = new ScmConfiguration();
configuration.setNamespaceStrategy("Arthur");
NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration);
NamespaceStrategy strategy = provider.get();
assertThat(strategy).isInstanceOf(Arthur.class);
}
@Test
void shouldReturnUsernameStrategyForUnknown() {
Set<NamespaceStrategy> strategies = Collections.emptySet();
ScmConfiguration configuration = new ScmConfiguration();
configuration.setNamespaceStrategy("Arthur");
NamespaceStrategyProvider provider = new NamespaceStrategyProvider(strategies, configuration);
NamespaceStrategy strategy = provider.get();
assertThat(strategy).isInstanceOf(UsernameNamespaceStrategy.class);
}
private LinkedHashSet<NamespaceStrategy> allStrategiesAsSet() {
return new LinkedHashSet<>(Arrays.asList(new Trillian(), new Zaphod(), new Arthur()));
}
private static class Trillian implements NamespaceStrategy{
@Override
public String createNamespace(Repository repository) {
return "trillian";
}
}
private static class Zaphod implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return "zaphod";
}
}
private static class Arthur implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return "arthur";
}
}
}

View File

@@ -0,0 +1,33 @@
package sonia.scm.repository;
import com.google.common.collect.Sets;
import org.junit.jupiter.api.Test;
import sonia.scm.ScmConstraintViolationException;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
class NamespaceStrategyValidatorTest {
@Test
void shouldThrowConstraintValidationException() {
NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Collections.emptySet());
assertThrows(ScmConstraintViolationException.class, () -> validator.check("AwesomeStrategy"));
}
@Test
void shouldDoNotThrowAnException() {
NamespaceStrategyValidator validator = new NamespaceStrategyValidator(Sets.newHashSet(new AwesomeStrategy()));
validator.check("AwesomeStrategy");
}
public static class AwesomeStrategy implements NamespaceStrategy {
@Override
public String createNamespace(Repository repository) {
return null;
}
}
}

View File

@@ -0,0 +1,20 @@
package sonia.scm.repository;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class RepositoryTypeNamespaceStrategyTest {
private final RepositoryTypeNamespaceStrategy namespaceStrategy = new RepositoryTypeNamespaceStrategy();
@Test
void shouldReturnTypeOfRepository() {
Repository git = RepositoryTestData.create42Puzzle("git");
assertThat(namespaceStrategy.createNamespace(git)).isEqualTo("git");
Repository hg = RepositoryTestData.create42Puzzle("hg");
assertThat(namespaceStrategy.createNamespace(hg)).isEqualTo("hg");
}
}

View File

@@ -0,0 +1,42 @@
package sonia.scm.repository;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class UsernameNamespaceStrategyTest {
@Mock
private Subject subject;
private final NamespaceStrategy usernameNamespaceStrategy = new UsernameNamespaceStrategy();
@BeforeEach
void setupSubject() {
ThreadContext.bind(subject);
}
@AfterEach
void clearThreadContext() {
ThreadContext.unbindSubject();
}
@Test
void shouldReturnPrimaryPrincipal() {
when(subject.getPrincipal()).thenReturn("trillian");
String namespace = usernameNamespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold());
assertThat(namespace).isEqualTo("trillian");
}
}

View File

@@ -47,22 +47,16 @@ import org.junit.runner.RunWith;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.junit.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import sonia.scm.group.GroupNames; import sonia.scm.group.ExternalGroupNames;
import java.util.Arrays; import java.util.Arrays;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.hamcrest.Matchers.*; import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static sonia.scm.security.SecureKeyTestUtil.createSecureKey; import static sonia.scm.security.SecureKeyTestUtil.createSecureKey;
/** /**
@@ -182,7 +176,11 @@ public class JwtAccessTokenBuilderTest {
private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable { private Object enrichWithGroups(InvocationOnMock invocation, String[] groups, boolean external) throws Throwable {
PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod()); PrincipalCollection principals = (PrincipalCollection) spy(invocation.callRealMethod());
when(principals.oneByType(GroupNames.class)).thenReturn(new GroupNames(Arrays.asList(groups), external));
List<String> groupCollection = Arrays.asList(groups);
if (external) {
when(principals.oneByType(ExternalGroupNames.class)).thenReturn(new ExternalGroupNames(groupCollection));
}
return principals; return principals;
} }