mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-08 06:25:45 +01:00
Feature/unicode groupname validation (#1600)
Allow all UTF-8 characters except URL identifiers as user and group names and for namespaces. Fixes #1513 Co-authored-by: René Pfeuffer <rene.pfeuffer@cloudogu.com>
This commit is contained in:
2
gradle/changelog/unicode_name_validation.yaml
Normal file
2
gradle/changelog/unicode_name_validation.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
- type: changed
|
||||||
|
description: Allow all UTF-8 characters except URL identifiers as user and group names and for namespaces. ([#1600](https://github.com/scm-manager/scm-manager/pull/1600))
|
||||||
@@ -243,7 +243,7 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public boolean isValid() {
|
public boolean isValid() {
|
||||||
return ValidationUtil.isRepositoryNameValid(namespace)
|
return ValidationUtil.isNameValid(namespace)
|
||||||
&& ValidationUtil.isRepositoryNameValid(name)
|
&& ValidationUtil.isRepositoryNameValid(name)
|
||||||
&& Util.isNotEmpty(type)
|
&& Util.isNotEmpty(type)
|
||||||
&& ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
|
&& ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ package sonia.scm.repository.spi;
|
|||||||
|
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
import sonia.scm.repository.api.ScmProtocol;
|
import sonia.scm.repository.api.ScmProtocol;
|
||||||
|
import sonia.scm.util.HttpUtil;
|
||||||
|
|
||||||
import javax.servlet.ServletConfig;
|
import javax.servlet.ServletConfig;
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
@@ -51,7 +52,7 @@ public abstract class HttpScmProtocol implements ScmProtocol {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUrl() {
|
public String getUrl() {
|
||||||
return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString();
|
return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", HttpUtil.encode(repository.getNamespace()), HttpUtil.encode(repository.getName()))).toASCIIString();
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
|
public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException {
|
||||||
|
|||||||
@@ -24,103 +24,66 @@
|
|||||||
|
|
||||||
package sonia.scm.util;
|
package sonia.scm.util;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
|
||||||
|
|
||||||
import sonia.scm.Validateable;
|
import sonia.scm.Validateable;
|
||||||
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
//~--- JDK imports ------------------------------------------------------------
|
public final class ValidationUtil {
|
||||||
|
|
||||||
/**
|
private static final String REGEX_MAIL = "^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$";
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
|
||||||
*/
|
|
||||||
public final class ValidationUtil
|
|
||||||
{
|
|
||||||
|
|
||||||
/** Field description */
|
public static final String REGEX_NAME = "^(?:(?:[^:/?#;&=\\s@%\\\\][^:/?#;&=%\\\\]*[^:/?#;&=\\s%\\\\])|(?:[^:/?#;&=\\s@%\\\\]))$";
|
||||||
private static final String REGEX_MAIL =
|
|
||||||
"^[A-Za-z0-9][\\w.-]*@[A-Za-z0-9][\\w\\-\\.]*\\.[A-Za-z0-9][A-Za-z0-9-]+$";
|
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
public static final String REGEX_NAME =
|
|
||||||
"^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$";
|
|
||||||
|
|
||||||
public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])(?!.*[.]git$)^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$";
|
public static final String REGEX_REPOSITORYNAME = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])(?!.*[.]git$)^[A-Za-z0-9\\.][A-Za-z0-9\\.\\-_]*$";
|
||||||
|
|
||||||
/** Field description */
|
|
||||||
private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);
|
private static final Pattern PATTERN_REPOSITORYNAME = Pattern.compile(REGEX_REPOSITORYNAME);
|
||||||
|
|
||||||
//~--- constructors ---------------------------------------------------------
|
private ValidationUtil() {
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs ...
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
private ValidationUtil() {}
|
|
||||||
|
|
||||||
//~--- get methods ----------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param value
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean isFilenameValid(String value)
|
|
||||||
{
|
|
||||||
return Util.isNotEmpty(value) && isNotContaining(value, "/", "\\", ":");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Returns {@code true} if the filename is valid.
|
||||||
*
|
*
|
||||||
*
|
* @param filename filename to be validated
|
||||||
* @param value
|
* @return {@code true} if filename is valid
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static boolean isMailAddressValid(String value)
|
public static boolean isFilenameValid(String filename) {
|
||||||
{
|
return Util.isNotEmpty(filename) && isNotContaining(filename, "/", "\\", ":");
|
||||||
return Util.isNotEmpty(value) && value.matches(REGEX_MAIL);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Returns {@code true} if the mail is valid.
|
||||||
*
|
*
|
||||||
*
|
* @param mail email-address to be validated
|
||||||
* @param name
|
* @return {@code true} if mail is valid
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static boolean isNameValid(String name)
|
public static boolean isMailAddressValid(String mail) {
|
||||||
{
|
return Util.isNotEmpty(mail) && mail.matches(REGEX_MAIL);
|
||||||
return Util.isNotEmpty(name) && name.matches(REGEX_NAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Returns {@code true} if the name is valid.
|
||||||
*
|
*
|
||||||
*
|
* @param name name to be validated
|
||||||
* @param value
|
* @return {@code true} if name is valid
|
||||||
* @param notAllowedStrings
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static boolean isNotContaining(String value,
|
public static boolean isNameValid(String name) {
|
||||||
String... notAllowedStrings)
|
return Util.isNotEmpty(name) && name.matches(REGEX_NAME) && !name.equals("..");
|
||||||
{
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if the object is valid.
|
||||||
|
*
|
||||||
|
* @param value value to be checked
|
||||||
|
* @param notAllowedStrings one or more strings which should not be included in value
|
||||||
|
* @return {@code true} if string has no not allowed strings else false
|
||||||
|
*/
|
||||||
|
public static boolean isNotContaining(String value, String... notAllowedStrings) {
|
||||||
boolean result = Util.isNotEmpty(value);
|
boolean result = Util.isNotEmpty(value);
|
||||||
|
|
||||||
if (result && (notAllowedStrings != null))
|
if (result && (notAllowedStrings != null)) {
|
||||||
{
|
for (String nas : notAllowedStrings) {
|
||||||
for (String nas : notAllowedStrings)
|
if (value.contains(nas)) {
|
||||||
{
|
|
||||||
if (value.indexOf(nas) >= 0)
|
|
||||||
{
|
|
||||||
result = false;
|
result = false;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -135,24 +98,20 @@ public final class ValidationUtil
|
|||||||
* Returns {@code true} if the repository name is valid.
|
* Returns {@code true} if the repository name is valid.
|
||||||
*
|
*
|
||||||
* @param name repository name
|
* @param name repository name
|
||||||
* @since 1.9
|
|
||||||
*
|
|
||||||
* @return {@code true} if repository name is valid
|
* @return {@code true} if repository name is valid
|
||||||
|
* @since 1.9
|
||||||
*/
|
*/
|
||||||
public static boolean isRepositoryNameValid(String name) {
|
public static boolean isRepositoryNameValid(String name) {
|
||||||
return PATTERN_REPOSITORYNAME.matcher(name).matches();
|
return PATTERN_REPOSITORYNAME.matcher(name).matches();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method description
|
* Returns {@code true} if the object is valid.
|
||||||
*
|
*
|
||||||
*
|
* @param validatable object to be validated
|
||||||
* @param validateable
|
* @return {@code true} if object is valid
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
*/
|
||||||
public static boolean isValid(Validateable validateable)
|
public static boolean isValid(Validateable validatable) {
|
||||||
{
|
return (validatable != null) && validatable.isValid();
|
||||||
return (validateable != null) && validateable.isValid();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,10 @@
|
|||||||
|
|
||||||
package sonia.scm.repository.spi;
|
package sonia.scm.repository.spi;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DynamicTest;
|
import org.junit.jupiter.api.DynamicTest;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestFactory;
|
import org.junit.jupiter.api.TestFactory;
|
||||||
import sonia.scm.repository.Repository;
|
import sonia.scm.repository.Repository;
|
||||||
|
|
||||||
@@ -37,6 +40,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
class HttpScmProtocolTest {
|
class HttpScmProtocolTest {
|
||||||
|
|
||||||
|
private String namespace;
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithSimpleNamespaceAndName {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setNamespaceAndName() {
|
||||||
|
namespace = "space";
|
||||||
|
name = "name";
|
||||||
|
}
|
||||||
|
|
||||||
@TestFactory
|
@TestFactory
|
||||||
Stream<DynamicTest> shouldCreateCorrectUrlsWithContextPath() {
|
Stream<DynamicTest> shouldCreateCorrectUrlsWithContextPath() {
|
||||||
return Stream.of("http://localhost/scm", "http://localhost/scm/")
|
return Stream.of("http://localhost/scm", "http://localhost/scm/")
|
||||||
@@ -48,6 +63,22 @@ class HttpScmProtocolTest {
|
|||||||
return Stream.of("http://localhost:8080", "http://localhost:8080/")
|
return Stream.of("http://localhost:8080", "http://localhost:8080/")
|
||||||
.map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name"));
|
.map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WithComplexNamespaceAndName{
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setNamespaceAndName() {
|
||||||
|
namespace = "name space";
|
||||||
|
name = "name";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateCorrectUrlsWithContextPath() {
|
||||||
|
assertResultingUrl("http://localhost/scm", "http://localhost/scm/repo/name%20space/name");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) {
|
DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) {
|
||||||
String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl();
|
String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl();
|
||||||
@@ -55,7 +86,7 @@ class HttpScmProtocolTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) {
|
private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) {
|
||||||
return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) {
|
return new HttpScmProtocol(new Repository("", "", namespace, name), baseUrl) {
|
||||||
@Override
|
@Override
|
||||||
protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
|
protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,105 +24,116 @@
|
|||||||
|
|
||||||
package sonia.scm.util;
|
package sonia.scm.util;
|
||||||
|
|
||||||
//~--- non-JDK imports --------------------------------------------------------
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
|
||||||
import org.junit.Test;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.junit.Assert.*;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author Sebastian Sdorra
|
* @author Sebastian Sdorra
|
||||||
*/
|
*/
|
||||||
public class ValidationUtilTest
|
class ValidationUtilTest {
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
@ParameterizedTest
|
||||||
* Method description
|
@ValueSource(strings = {
|
||||||
*
|
"test",
|
||||||
*/
|
"test 123"
|
||||||
@Test
|
})
|
||||||
public void testIsFilenameValid()
|
void shouldAcceptFilename(String value) {
|
||||||
{
|
assertTrue(ValidationUtil.isFilenameValid(value));
|
||||||
|
|
||||||
// true
|
|
||||||
assertTrue(ValidationUtil.isFilenameValid("test"));
|
|
||||||
assertTrue(ValidationUtil.isFilenameValid("test 123"));
|
|
||||||
|
|
||||||
// false
|
|
||||||
assertFalse(ValidationUtil.isFilenameValid("../../"));
|
|
||||||
assertFalse(ValidationUtil.isFilenameValid("test/../.."));
|
|
||||||
assertFalse(ValidationUtil.isFilenameValid("\\ka"));
|
|
||||||
assertFalse(ValidationUtil.isFilenameValid("ka:on"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@ParameterizedTest
|
||||||
* Method description
|
@ValueSource(strings = {
|
||||||
*
|
"../../",
|
||||||
*/
|
"test/../..",
|
||||||
@Test
|
"\\ka, \"ka:on\""
|
||||||
public void testIsMailAddressValid()
|
})
|
||||||
{
|
void shouldRejectFilename(String value) {
|
||||||
|
assertFalse(ValidationUtil.isFilenameValid(value));
|
||||||
// true
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@ostfalia.de"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("sdorra@ostfalia.de"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@hbk-bs.de"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@gmail.com"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@t.co"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@ucla.college"));
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@example.xn--p1ai"));
|
|
||||||
|
|
||||||
// issue 909
|
|
||||||
assertTrue(ValidationUtil.isMailAddressValid("s.sdorra@scm.solutions"));
|
|
||||||
|
|
||||||
// false
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("ostfalia.de"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("@ostfalia.de"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@ostfalia"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@@ostfalia.de"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("s.sdorra@ ostfalia.de"));
|
|
||||||
assertFalse(ValidationUtil.isMailAddressValid("s.sdorra @ostfalia.de"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@ParameterizedTest
|
||||||
* Method description
|
@ValueSource(strings = {
|
||||||
*
|
"s.sdorra@ostfalia.de",
|
||||||
*/
|
"sdorra@ostfalia.de",
|
||||||
@Test
|
"s.sdorra@hbk-bs.de",
|
||||||
public void testIsNameValid()
|
"s.sdorra@gmail.com",
|
||||||
{
|
"s.sdorra@t.co",
|
||||||
|
"s.sdorra@ucla.college",
|
||||||
// true
|
"s.sdorra@example.xn--p1ai",
|
||||||
assertTrue(ValidationUtil.isNameValid("test"));
|
"s.sdorra@scm.solutions" // issue 909
|
||||||
assertTrue(ValidationUtil.isNameValid("test.git"));
|
})
|
||||||
assertTrue(ValidationUtil.isNameValid("Test123.git"));
|
void shouldAcceptMailAddress(String value) {
|
||||||
assertTrue(ValidationUtil.isNameValid("Test123-git"));
|
assertTrue(ValidationUtil.isMailAddressValid(value));
|
||||||
assertTrue(ValidationUtil.isNameValid("Test_user-123.git"));
|
}
|
||||||
assertTrue(ValidationUtil.isNameValid("test@scm-manager.de"));
|
|
||||||
assertTrue(ValidationUtil.isNameValid("t"));
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
// false
|
"ostfalia.de",
|
||||||
assertFalse(ValidationUtil.isNameValid("test 123"));
|
"@ostfalia.de",
|
||||||
assertFalse(ValidationUtil.isNameValid(" test 123"));
|
"s.sdorra@",
|
||||||
assertFalse(ValidationUtil.isNameValid(" test 123 "));
|
"s.sdorra@ostfalia",
|
||||||
assertFalse(ValidationUtil.isNameValid("test 123 "));
|
"s.sdorra@@ostfalia.de",
|
||||||
assertFalse(ValidationUtil.isNameValid("test/123"));
|
"s.sdorra@ ostfalia.de",
|
||||||
assertFalse(ValidationUtil.isNameValid("test%123"));
|
"s.sdorra @ostfalia.de"
|
||||||
assertFalse(ValidationUtil.isNameValid("test:123"));
|
})
|
||||||
assertFalse(ValidationUtil.isNameValid("t "));
|
void shouldRejectMailAddress(String value) {
|
||||||
assertFalse(ValidationUtil.isNameValid(" t"));
|
assertFalse(ValidationUtil.isMailAddressValid(value));
|
||||||
assertFalse(ValidationUtil.isNameValid(" t "));
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"test",
|
||||||
|
"test.git",
|
||||||
|
"Test123.git",
|
||||||
|
"Test123-git",
|
||||||
|
"Test_user-123.git",
|
||||||
|
"test@scm-manager.de",
|
||||||
|
"t",
|
||||||
|
"Лорем-ипсум",
|
||||||
|
"Λορεμ.ιπσθμ",
|
||||||
|
"լոռեմիպսում",
|
||||||
|
"ლორემიფსუმ",
|
||||||
|
"प्रमान",
|
||||||
|
"詳性約",
|
||||||
|
"隠サレニ",
|
||||||
|
"법률",
|
||||||
|
"المدن",
|
||||||
|
"אחד",
|
||||||
|
"Hu-rëm"
|
||||||
|
})
|
||||||
|
void shouldAcceptName(String value) {
|
||||||
|
assertTrue(ValidationUtil.isNameValid(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {
|
||||||
|
"@",
|
||||||
|
"@test",
|
||||||
|
" test123",
|
||||||
|
"test/123",
|
||||||
|
"test:123",
|
||||||
|
"test#123",
|
||||||
|
"test%123",
|
||||||
|
"test&123",
|
||||||
|
"test?123",
|
||||||
|
"test=123",
|
||||||
|
"test;123",
|
||||||
|
"@test123",
|
||||||
|
"t ",
|
||||||
|
" t",
|
||||||
|
" t ",
|
||||||
|
".."
|
||||||
|
})
|
||||||
|
void shouldRejectName(String value) {
|
||||||
|
assertFalse(ValidationUtil.isNameValid(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method description
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Test
|
@Test
|
||||||
public void testIsNotContaining()
|
void testIsNotContaining() {
|
||||||
{
|
|
||||||
|
|
||||||
// true
|
// true
|
||||||
assertTrue(ValidationUtil.isNotContaining("test", "abc"));
|
assertTrue(ValidationUtil.isNotContaining("test", "abc"));
|
||||||
@@ -134,9 +145,8 @@ public class ValidationUtilTest
|
|||||||
assertFalse(ValidationUtil.isNotContaining("test", "t"));
|
assertFalse(ValidationUtil.isNotContaining("test", "t"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@ParameterizedTest
|
||||||
public void testIsRepositoryNameValid() {
|
@ValueSource(strings = {
|
||||||
String[] validPaths = {
|
|
||||||
"scm",
|
"scm",
|
||||||
"scm-",
|
"scm-",
|
||||||
"scm_",
|
"scm_",
|
||||||
@@ -150,10 +160,13 @@ public class ValidationUtilTest
|
|||||||
"..c",
|
"..c",
|
||||||
"d..",
|
"d..",
|
||||||
"a..c"
|
"a..c"
|
||||||
};
|
})
|
||||||
|
void shouldAcceptRepositoryName(String path) {
|
||||||
|
assertTrue(ValidationUtil.isRepositoryNameValid(path));
|
||||||
|
}
|
||||||
|
|
||||||
// issue 142, 144 and 148
|
@ParameterizedTest
|
||||||
String[] invalidPaths = {
|
@ValueSource(strings = {
|
||||||
".",
|
".",
|
||||||
"/",
|
"/",
|
||||||
"//",
|
"//",
|
||||||
@@ -205,14 +218,8 @@ public class ValidationUtilTest
|
|||||||
"-scm",
|
"-scm",
|
||||||
"scm.git",
|
"scm.git",
|
||||||
"scm.git.git"
|
"scm.git.git"
|
||||||
};
|
})
|
||||||
|
void shouldRejectRepositoryName(String path) {
|
||||||
for (String path : validPaths) {
|
|
||||||
assertTrue(ValidationUtil.isRepositoryNameValid(path));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (String path : invalidPaths) {
|
|
||||||
assertFalse(ValidationUtil.isRepositoryNameValid(path));
|
assertFalse(ValidationUtil.isRepositoryNameValid(path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
/*
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package sonia.scm.util;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.Parameterized;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
import static org.junit.Assert.assertFalse;
|
|
||||||
import static sonia.scm.util.ValidationUtil.REGEX_NAME;
|
|
||||||
|
|
||||||
@RunWith(Parameterized.class)
|
|
||||||
public class ValidationUtil_IllegalCharactersTest {
|
|
||||||
|
|
||||||
private static final List<Character> ACCEPTED_CHARS = asList('@', '_', '-', '.');
|
|
||||||
|
|
||||||
private final Pattern userGroupPattern=Pattern.compile(REGEX_NAME);
|
|
||||||
|
|
||||||
private final String expression;
|
|
||||||
|
|
||||||
public ValidationUtil_IllegalCharactersTest(String expression) {
|
|
||||||
this.expression = expression;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Parameterized.Parameters(name = "{0}")
|
|
||||||
public static Collection<String[]> createParameters() {
|
|
||||||
return Stream.concat(IntStream.range(0x20, 0x2f).mapToObj(i -> (char) i), // chars before '0'
|
|
||||||
Stream.concat(IntStream.range(0x3a, 0x40).mapToObj(i -> (char) i), // chars between '9' and 'A'
|
|
||||||
Stream.concat(IntStream.range(0x5b, 0x60).mapToObj(i -> (char) i), // chars between 'Z' and 'a'
|
|
||||||
IntStream.range(0x7b, 0xff).mapToObj(i -> (char) i)))) // chars after 'z'
|
|
||||||
.filter(c -> !ACCEPTED_CHARS.contains(c))
|
|
||||||
.flatMap(c -> Stream.of("abc" + c + "xyz", "@" + c, c + "tail"))
|
|
||||||
.map(c -> new String[] {c})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void shouldNotAcceptSpecialCharacters() {
|
|
||||||
assertFalse(userGroupPattern.matcher(expression).matches());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -46426,7 +46426,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
||||||
@@ -46450,7 +46449,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
||||||
@@ -46504,7 +46502,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
||||||
@@ -46529,7 +46526,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
|||||||
value=""
|
value=""
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ type Props = {
|
|||||||
onReturnPressed?: () => void;
|
onReturnPressed?: () => void;
|
||||||
validationError?: boolean;
|
validationError?: boolean;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
|
informationMessage?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -86,6 +87,7 @@ class InputField extends React.Component<Props> {
|
|||||||
value,
|
value,
|
||||||
validationError,
|
validationError,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
informationMessage,
|
||||||
disabled,
|
disabled,
|
||||||
label,
|
label,
|
||||||
helpText,
|
helpText,
|
||||||
@@ -93,7 +95,12 @@ class InputField extends React.Component<Props> {
|
|||||||
testId
|
testId
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const errorView = validationError ? "is-danger" : "";
|
const errorView = validationError ? "is-danger" : "";
|
||||||
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
let helper;
|
||||||
|
if (validationError) {
|
||||||
|
helper = <p className="help is-danger">{errorMessage}</p>;
|
||||||
|
} else if (informationMessage) {
|
||||||
|
helper = <p className="help is-info">{informationMessage}</p>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className={classNames("field", className)}>
|
<div className={classNames("field", className)}>
|
||||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { TFunction } from "i18next";
|
|||||||
import { AstPlugin } from "./PluginApi";
|
import { AstPlugin } from "./PluginApi";
|
||||||
import { Node, Parent } from "unist";
|
import { Node, Parent } from "unist";
|
||||||
|
|
||||||
const namePartRegex = nameRegex.source.substring(1, nameRegex.source.length - 1);
|
const namePartRegex = nameRegex.source.substring(1, nameRegex.source.length - 1).replace(/\[\^([^\]s]+)\]/, "[^$1\\s]");
|
||||||
|
|
||||||
export const regExpPattern = `(${namePartRegex})\\/(${namePartRegex})@([\\w\\d]+)`;
|
export const regExpPattern = `(${namePartRegex})\\/(${namePartRegex})@([\\w\\d]+)`;
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,15 @@ describe("test name validation", () => {
|
|||||||
" test 123 ",
|
" test 123 ",
|
||||||
"test 123 ",
|
"test 123 ",
|
||||||
"test/123",
|
"test/123",
|
||||||
"test%123",
|
|
||||||
"test:123",
|
"test:123",
|
||||||
"t ",
|
"t ",
|
||||||
" t",
|
" t",
|
||||||
" t ",
|
" t ",
|
||||||
"",
|
"",
|
||||||
" invalid_name",
|
" invalid_name",
|
||||||
"another%one",
|
"%",
|
||||||
"!!!",
|
"test%name",
|
||||||
"!_!"
|
"test\\name"
|
||||||
];
|
];
|
||||||
for (const name of invalidNames) {
|
for (const name of invalidNames) {
|
||||||
it(`should return false for '${name}'`, () => {
|
it(`should return false for '${name}'`, () => {
|
||||||
@@ -52,6 +51,7 @@ describe("test name validation", () => {
|
|||||||
// valid names taken from ValidationUtilTest.java
|
// valid names taken from ValidationUtilTest.java
|
||||||
const validNames = [
|
const validNames = [
|
||||||
"test",
|
"test",
|
||||||
|
"test git",
|
||||||
"test.git",
|
"test.git",
|
||||||
"Test123.git",
|
"Test123.git",
|
||||||
"Test123-git",
|
"Test123-git",
|
||||||
@@ -64,7 +64,18 @@ describe("test name validation", () => {
|
|||||||
"another1",
|
"another1",
|
||||||
"stillValid",
|
"stillValid",
|
||||||
"this.one_as-well",
|
"this.one_as-well",
|
||||||
"and@this"
|
"and@this",
|
||||||
|
"Лорем-ипсум",
|
||||||
|
"Λορεμ.ιπσθμ",
|
||||||
|
"լոռեմիպսում",
|
||||||
|
"ლორემიფსუმ",
|
||||||
|
"प्रमान",
|
||||||
|
"詳性約",
|
||||||
|
"隠サレニ",
|
||||||
|
"법률",
|
||||||
|
"المدن",
|
||||||
|
"אחד",
|
||||||
|
"Hu-rëm"
|
||||||
];
|
];
|
||||||
for (const name of validNames) {
|
for (const name of validNames) {
|
||||||
it(`should return true for '${name}'`, () => {
|
it(`should return true for '${name}'`, () => {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
|
export const nameRegex = /^(?:(?:[^:/?#;&=\s@%\\][^:/?#;&=%\\]*[^:/?#;&=\s%\\]+)|[^:/?#;&=\s@%\\])$/;
|
||||||
|
|
||||||
export const isNameValid = (name: string) => {
|
export const isNameValid = (name: string) => {
|
||||||
return nameRegex.test(name);
|
return nameRegex.test(name);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
"namespace-invalid": "Der Namespace des Repository ist ungültig",
|
||||||
|
"namespaceSpaceWarningText": "Achtung: Leerzeichen in Namespaces funktionieren prinzipiell, können jedoch bei einigen Funktionen zu Problemen führen",
|
||||||
"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",
|
||||||
"url-invalid": "Die URL ist ungültig",
|
"url-invalid": "Die URL ist ungültig",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"namespace-invalid": "The repository namespace is invalid",
|
"namespace-invalid": "The repository namespace is invalid",
|
||||||
|
"namespaceSpaceWarningText": "Warning: Spaces in namespaces will work in principle, but can lead to problems in some features",
|
||||||
"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",
|
||||||
"url-invalid": "The URL is invalid",
|
"url-invalid": "The URL is invalid",
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderNamespaceField = () => {
|
const renderNamespaceField = () => {
|
||||||
|
let informationMessage = undefined;
|
||||||
|
if (repository?.namespace?.indexOf(" ") > 0) {
|
||||||
|
informationMessage = t("validation.namespaceSpaceWarningText");
|
||||||
|
}
|
||||||
|
|
||||||
const props = {
|
const props = {
|
||||||
label: t("repository.namespace"),
|
label: t("repository.namespace"),
|
||||||
helpText: t("help.namespaceHelpText"),
|
helpText: t("help.namespaceHelpText"),
|
||||||
@@ -90,7 +95,8 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
|
|||||||
onChange: handleNamespaceChange,
|
onChange: handleNamespaceChange,
|
||||||
errorMessage: t("validation.namespace-invalid"),
|
errorMessage: t("validation.namespace-invalid"),
|
||||||
validationError: namespaceValidationError,
|
validationError: namespaceValidationError,
|
||||||
disabled: disabled
|
disabled: disabled,
|
||||||
|
informationMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||||
|
|||||||
@@ -23,12 +23,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { validation } from "@scm-manager/ui-components";
|
import { validation } from "@scm-manager/ui-components";
|
||||||
|
import { isNameValid as isUserNameValid } from "../../../users/components/userValidation";
|
||||||
|
|
||||||
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[.]git$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
|
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[.]git$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
|
||||||
const namespaceExceptionsRegex = /^(([0-9]{1,3})|(create)|(import))$/;
|
const namespaceExceptionsRegex = /^(([0-9]{1,3})|(create)|(import))$/;
|
||||||
|
|
||||||
export const isNamespaceValid = (name: string) => {
|
export const isNamespaceValid = (name: string) => {
|
||||||
return nameRegex.test(name) && !namespaceExceptionsRegex.test(name);
|
return isUserNameValid(name) && !namespaceExceptionsRegex.test(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isNameValid = (name: string) => {
|
export const isNameValid = (name: string) => {
|
||||||
|
|||||||
@@ -43,9 +43,12 @@ public class CustomNamespaceStrategy implements NamespaceStrategy {
|
|||||||
doThrow()
|
doThrow()
|
||||||
.violation("invalid namespace", "namespace")
|
.violation("invalid namespace", "namespace")
|
||||||
.when(
|
.when(
|
||||||
!ValidationUtil.isRepositoryNameValid(namespace)
|
!ValidationUtil.isNameValid(namespace)
|
||||||
|| ONE_TO_THREE_DIGITS.matcher(namespace).matches()
|
|| ONE_TO_THREE_DIGITS.matcher(namespace).matches()
|
||||||
|| namespace.equals("create"));
|
|| namespace.equals("create")
|
||||||
|
|| namespace.equals("import")
|
||||||
|
|| namespace.equals("..")
|
||||||
|
);
|
||||||
|
|
||||||
return namespace;
|
return namespace;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,6 @@ public class UsernameNamespaceStrategy implements NamespaceStrategy {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String createNamespace(Repository repository) {
|
public String createNamespace(Repository repository) {
|
||||||
return SecurityUtils.getSubject().getPrincipal().toString();
|
return SecurityUtils.getSubject().getPrincipal().toString().replaceAll("\\s", "_");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ public class GroupRootResourceTest {
|
|||||||
@Rule
|
@Rule
|
||||||
public ShiroRule shiro = new ShiroRule();
|
public ShiroRule shiro = new ShiroRule();
|
||||||
|
|
||||||
private RestDispatcher dispatcher = new RestDispatcher();
|
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||||
|
|
||||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||||
|
|
||||||
@@ -284,27 +284,6 @@ public class GroupRootResourceTest {
|
|||||||
|
|
||||||
assertEquals(400, response.getStatus());
|
assertEquals(400, response.getStatus());
|
||||||
|
|
||||||
// the characters {[ are not allowed
|
|
||||||
groupJson = "{ \"name\": \"grp{name}\", \"type\": \"admin\" }";
|
|
||||||
request = MockHttpRequest
|
|
||||||
.post("/" + GroupRootResource.GROUPS_PATH_V2)
|
|
||||||
.contentType(VndMediaType.GROUP)
|
|
||||||
.content(groupJson.getBytes());
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
|
||||||
|
|
||||||
assertEquals(400, response.getStatus());
|
|
||||||
|
|
||||||
groupJson = "{ \"name\": \"grp[name]\", \"type\": \"admin\" }";
|
|
||||||
request = MockHttpRequest
|
|
||||||
.post("/" + GroupRootResource.GROUPS_PATH_V2)
|
|
||||||
.contentType(VndMediaType.GROUP)
|
|
||||||
.content(groupJson.getBytes());
|
|
||||||
|
|
||||||
dispatcher.invoke(request, response);
|
|
||||||
|
|
||||||
assertEquals(400, response.getStatus());
|
|
||||||
|
|
||||||
groupJson = "{ \"name\": \"grp/name\", \"type\": \"admin\" }";
|
groupJson = "{ \"name\": \"grp/name\", \"type\": \"admin\" }";
|
||||||
request = MockHttpRequest
|
request = MockHttpRequest
|
||||||
.post("/" + GroupRootResource.GROUPS_PATH_V2)
|
.post("/" + GroupRootResource.GROUPS_PATH_V2)
|
||||||
|
|||||||
@@ -63,4 +63,11 @@ class UsernameNamespaceStrategyTest {
|
|||||||
assertThat(namespace).isEqualTo("trillian");
|
assertThat(namespace).isEqualTo("trillian");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReplaceSpacesInPrincipalName() {
|
||||||
|
when(subject.getPrincipal()).thenReturn("arthur dent");
|
||||||
|
|
||||||
|
String namespace = usernameNamespaceStrategy.createNamespace(RepositoryTestData.createHeartOfGold());
|
||||||
|
assertThat(namespace).isEqualTo("arthur_dent");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user