mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 14:05:44 +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
|
||||
public boolean isValid() {
|
||||
return ValidationUtil.isRepositoryNameValid(namespace)
|
||||
return ValidationUtil.isNameValid(namespace)
|
||||
&& ValidationUtil.isRepositoryNameValid(name)
|
||||
&& Util.isNotEmpty(type)
|
||||
&& ((Util.isEmpty(contact)) || ValidationUtil.isMailAddressValid(contact));
|
||||
|
||||
@@ -26,6 +26,7 @@ package sonia.scm.repository.spi;
|
||||
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.ScmProtocol;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
@@ -51,7 +52,7 @@ public abstract class HttpScmProtocol implements ScmProtocol {
|
||||
|
||||
@Override
|
||||
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 {
|
||||
|
||||
@@ -24,103 +24,66 @@
|
||||
|
||||
package sonia.scm.util;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import sonia.scm.Validateable;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
public final class ValidationUtil {
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
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-]+$";
|
||||
|
||||
/** Field description */
|
||||
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_NAME = "^(?:(?:[^:/?#;&=\\s@%\\\\][^:/?#;&=%\\\\]*[^:/?#;&=\\s%\\\\])|(?:[^:/?#;&=\\s@%\\\\]))$";
|
||||
|
||||
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);
|
||||
|
||||
//~--- constructors ---------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Constructs ...
|
||||
*
|
||||
*/
|
||||
private ValidationUtil() {}
|
||||
|
||||
//~--- get methods ----------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static boolean isFilenameValid(String value)
|
||||
{
|
||||
return Util.isNotEmpty(value) && isNotContaining(value, "/", "\\", ":");
|
||||
private ValidationUtil() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Returns {@code true} if the filename is valid.
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
*
|
||||
* @return
|
||||
* @param filename filename to be validated
|
||||
* @return {@code true} if filename is valid
|
||||
*/
|
||||
public static boolean isMailAddressValid(String value)
|
||||
{
|
||||
return Util.isNotEmpty(value) && value.matches(REGEX_MAIL);
|
||||
public static boolean isFilenameValid(String filename) {
|
||||
return Util.isNotEmpty(filename) && isNotContaining(filename, "/", "\\", ":");
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Returns {@code true} if the mail is valid.
|
||||
*
|
||||
*
|
||||
* @param name
|
||||
*
|
||||
* @return
|
||||
* @param mail email-address to be validated
|
||||
* @return {@code true} if mail is valid
|
||||
*/
|
||||
public static boolean isNameValid(String name)
|
||||
{
|
||||
return Util.isNotEmpty(name) && name.matches(REGEX_NAME);
|
||||
public static boolean isMailAddressValid(String mail) {
|
||||
return Util.isNotEmpty(mail) && mail.matches(REGEX_MAIL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Returns {@code true} if the name is valid.
|
||||
*
|
||||
*
|
||||
* @param value
|
||||
* @param notAllowedStrings
|
||||
*
|
||||
* @return
|
||||
* @param name name to be validated
|
||||
* @return {@code true} if name is valid
|
||||
*/
|
||||
public static boolean isNotContaining(String value,
|
||||
String... notAllowedStrings)
|
||||
{
|
||||
public static boolean isNameValid(String name) {
|
||||
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);
|
||||
|
||||
if (result && (notAllowedStrings != null))
|
||||
{
|
||||
for (String nas : notAllowedStrings)
|
||||
{
|
||||
if (value.indexOf(nas) >= 0)
|
||||
{
|
||||
if (result && (notAllowedStrings != null)) {
|
||||
for (String nas : notAllowedStrings) {
|
||||
if (value.contains(nas)) {
|
||||
result = false;
|
||||
|
||||
break;
|
||||
@@ -135,24 +98,20 @@ public final class ValidationUtil
|
||||
* Returns {@code true} if the repository name is valid.
|
||||
*
|
||||
* @param name repository name
|
||||
* @since 1.9
|
||||
*
|
||||
* @return {@code true} if repository name is valid
|
||||
* @since 1.9
|
||||
*/
|
||||
public static boolean isRepositoryNameValid(String name) {
|
||||
return PATTERN_REPOSITORYNAME.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
* Returns {@code true} if the object is valid.
|
||||
*
|
||||
*
|
||||
* @param validateable
|
||||
*
|
||||
* @return
|
||||
* @param validatable object to be validated
|
||||
* @return {@code true} if object is valid
|
||||
*/
|
||||
public static boolean isValid(Validateable validateable)
|
||||
{
|
||||
return (validateable != null) && validateable.isValid();
|
||||
public static boolean isValid(Validateable validatable) {
|
||||
return (validatable != null) && validatable.isValid();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,10 @@
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 sonia.scm.repository.Repository;
|
||||
|
||||
@@ -37,6 +40,18 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class HttpScmProtocolTest {
|
||||
|
||||
private String namespace;
|
||||
private String name;
|
||||
|
||||
@Nested
|
||||
class WithSimpleNamespaceAndName {
|
||||
|
||||
@BeforeEach
|
||||
void setNamespaceAndName() {
|
||||
namespace = "space";
|
||||
name = "name";
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> shouldCreateCorrectUrlsWithContextPath() {
|
||||
return Stream.of("http://localhost/scm", "http://localhost/scm/")
|
||||
@@ -48,6 +63,22 @@ class HttpScmProtocolTest {
|
||||
return Stream.of("http://localhost:8080", "http://localhost:8080/")
|
||||
.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) {
|
||||
String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl();
|
||||
@@ -55,7 +86,7 @@ class HttpScmProtocolTest {
|
||||
}
|
||||
|
||||
private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) {
|
||||
return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) {
|
||||
return new HttpScmProtocol(new Repository("", "", namespace, name), baseUrl) {
|
||||
@Override
|
||||
protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) {
|
||||
}
|
||||
|
||||
@@ -24,105 +24,116 @@
|
||||
|
||||
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.Assert.*;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class ValidationUtilTest
|
||||
{
|
||||
class ValidationUtilTest {
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testIsFilenameValid()
|
||||
{
|
||||
|
||||
// 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
|
||||
@ValueSource(strings = {
|
||||
"test",
|
||||
"test 123"
|
||||
})
|
||||
void shouldAcceptFilename(String value) {
|
||||
assertTrue(ValidationUtil.isFilenameValid(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testIsMailAddressValid()
|
||||
{
|
||||
|
||||
// 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
|
||||
@ValueSource(strings = {
|
||||
"../../",
|
||||
"test/../..",
|
||||
"\\ka, \"ka:on\""
|
||||
})
|
||||
void shouldRejectFilename(String value) {
|
||||
assertFalse(ValidationUtil.isFilenameValid(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method description
|
||||
*
|
||||
*/
|
||||
@Test
|
||||
public void testIsNameValid()
|
||||
{
|
||||
|
||||
// true
|
||||
assertTrue(ValidationUtil.isNameValid("test"));
|
||||
assertTrue(ValidationUtil.isNameValid("test.git"));
|
||||
assertTrue(ValidationUtil.isNameValid("Test123.git"));
|
||||
assertTrue(ValidationUtil.isNameValid("Test123-git"));
|
||||
assertTrue(ValidationUtil.isNameValid("Test_user-123.git"));
|
||||
assertTrue(ValidationUtil.isNameValid("test@scm-manager.de"));
|
||||
assertTrue(ValidationUtil.isNameValid("t"));
|
||||
|
||||
// false
|
||||
assertFalse(ValidationUtil.isNameValid("test 123"));
|
||||
assertFalse(ValidationUtil.isNameValid(" test 123"));
|
||||
assertFalse(ValidationUtil.isNameValid(" test 123 "));
|
||||
assertFalse(ValidationUtil.isNameValid("test 123 "));
|
||||
assertFalse(ValidationUtil.isNameValid("test/123"));
|
||||
assertFalse(ValidationUtil.isNameValid("test%123"));
|
||||
assertFalse(ValidationUtil.isNameValid("test:123"));
|
||||
assertFalse(ValidationUtil.isNameValid("t "));
|
||||
assertFalse(ValidationUtil.isNameValid(" t"));
|
||||
assertFalse(ValidationUtil.isNameValid(" t "));
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"s.sdorra@ostfalia.de",
|
||||
"sdorra@ostfalia.de",
|
||||
"s.sdorra@hbk-bs.de",
|
||||
"s.sdorra@gmail.com",
|
||||
"s.sdorra@t.co",
|
||||
"s.sdorra@ucla.college",
|
||||
"s.sdorra@example.xn--p1ai",
|
||||
"s.sdorra@scm.solutions" // issue 909
|
||||
})
|
||||
void shouldAcceptMailAddress(String value) {
|
||||
assertTrue(ValidationUtil.isMailAddressValid(value));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"ostfalia.de",
|
||||
"@ostfalia.de",
|
||||
"s.sdorra@",
|
||||
"s.sdorra@ostfalia",
|
||||
"s.sdorra@@ostfalia.de",
|
||||
"s.sdorra@ ostfalia.de",
|
||||
"s.sdorra @ostfalia.de"
|
||||
})
|
||||
void shouldRejectMailAddress(String value) {
|
||||
assertFalse(ValidationUtil.isMailAddressValid(value));
|
||||
}
|
||||
|
||||
@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
|
||||
public void testIsNotContaining()
|
||||
{
|
||||
void testIsNotContaining() {
|
||||
|
||||
// true
|
||||
assertTrue(ValidationUtil.isNotContaining("test", "abc"));
|
||||
@@ -134,9 +145,8 @@ public class ValidationUtilTest
|
||||
assertFalse(ValidationUtil.isNotContaining("test", "t"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsRepositoryNameValid() {
|
||||
String[] validPaths = {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
"scm",
|
||||
"scm-",
|
||||
"scm_",
|
||||
@@ -150,10 +160,13 @@ public class ValidationUtilTest
|
||||
"..c",
|
||||
"d..",
|
||||
"a..c"
|
||||
};
|
||||
})
|
||||
void shouldAcceptRepositoryName(String path) {
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid(path));
|
||||
}
|
||||
|
||||
// issue 142, 144 and 148
|
||||
String[] invalidPaths = {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {
|
||||
".",
|
||||
"/",
|
||||
"//",
|
||||
@@ -205,14 +218,8 @@ public class ValidationUtilTest
|
||||
"-scm",
|
||||
"scm.git",
|
||||
"scm.git.git"
|
||||
};
|
||||
|
||||
for (String path : validPaths) {
|
||||
assertTrue(ValidationUtil.isRepositoryNameValid(path));
|
||||
}
|
||||
|
||||
for (String path : invalidPaths) {
|
||||
})
|
||||
void shouldRejectRepositoryName(String 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=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
||||
@@ -46450,7 +46449,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Default 1`] = `
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button
|
||||
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
||||
@@ -46504,7 +46502,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="field AddKeyValueEntryToTableField__StyledInputField-kiarql-1 hwPPZB"
|
||||
@@ -46529,7 +46526,6 @@ exports[`Storyshots Forms|AddKeyValueEntryToTableField Disabled 1`] = `
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<button
|
||||
className="button is-default AddKeyValueEntryToTableField__MarginTopButton-kiarql-2 hyqVki"
|
||||
|
||||
@@ -37,6 +37,7 @@ type Props = {
|
||||
onReturnPressed?: () => void;
|
||||
validationError?: boolean;
|
||||
errorMessage?: string;
|
||||
informationMessage?: string;
|
||||
disabled?: boolean;
|
||||
helpText?: string;
|
||||
className?: string;
|
||||
@@ -86,6 +87,7 @@ class InputField extends React.Component<Props> {
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
informationMessage,
|
||||
disabled,
|
||||
label,
|
||||
helpText,
|
||||
@@ -93,7 +95,12 @@ class InputField extends React.Component<Props> {
|
||||
testId
|
||||
} = this.props;
|
||||
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 (
|
||||
<div className={classNames("field", className)}>
|
||||
<LabelWithHelpIcon label={label} helpText={helpText} />
|
||||
|
||||
@@ -27,7 +27,7 @@ import { TFunction } from "i18next";
|
||||
import { AstPlugin } from "./PluginApi";
|
||||
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]+)`;
|
||||
|
||||
|
||||
@@ -32,16 +32,15 @@ describe("test name validation", () => {
|
||||
" test 123 ",
|
||||
"test 123 ",
|
||||
"test/123",
|
||||
"test%123",
|
||||
"test:123",
|
||||
"t ",
|
||||
" t",
|
||||
" t ",
|
||||
"",
|
||||
" invalid_name",
|
||||
"another%one",
|
||||
"!!!",
|
||||
"!_!"
|
||||
"%",
|
||||
"test%name",
|
||||
"test\\name"
|
||||
];
|
||||
for (const name of invalidNames) {
|
||||
it(`should return false for '${name}'`, () => {
|
||||
@@ -52,6 +51,7 @@ describe("test name validation", () => {
|
||||
// valid names taken from ValidationUtilTest.java
|
||||
const validNames = [
|
||||
"test",
|
||||
"test git",
|
||||
"test.git",
|
||||
"Test123.git",
|
||||
"Test123-git",
|
||||
@@ -64,7 +64,18 @@ describe("test name validation", () => {
|
||||
"another1",
|
||||
"stillValid",
|
||||
"this.one_as-well",
|
||||
"and@this"
|
||||
"and@this",
|
||||
"Лорем-ипсум",
|
||||
"Λορεμ.ιπσθμ",
|
||||
"լոռեմիպսում",
|
||||
"ლორემიფსუმ",
|
||||
"प्रमान",
|
||||
"詳性約",
|
||||
"隠サレニ",
|
||||
"법률",
|
||||
"المدن",
|
||||
"אחד",
|
||||
"Hu-rëm"
|
||||
];
|
||||
for (const name of validNames) {
|
||||
it(`should return true for '${name}'`, () => {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
export const nameRegex = /^[A-Za-z0-9\.\-_][A-Za-z0-9\.\-_@]*$/;
|
||||
export const nameRegex = /^(?:(?:[^:/?#;&=\s@%\\][^:/?#;&=%\\]*[^:/?#;&=\s%\\]+)|[^:/?#;&=\s@%\\])$/;
|
||||
|
||||
export const isNameValid = (name: string) => {
|
||||
return nameRegex.test(name);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"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",
|
||||
"contact-invalid": "Der Kontakt muss eine gültige E-Mail Adresse sein",
|
||||
"url-invalid": "Die URL ist ungültig",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
},
|
||||
"validation": {
|
||||
"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",
|
||||
"contact-invalid": "Contact must be a valid mail address",
|
||||
"url-invalid": "The URL is invalid",
|
||||
|
||||
@@ -83,6 +83,11 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
|
||||
};
|
||||
|
||||
const renderNamespaceField = () => {
|
||||
let informationMessage = undefined;
|
||||
if (repository?.namespace?.indexOf(" ") > 0) {
|
||||
informationMessage = t("validation.namespaceSpaceWarningText");
|
||||
}
|
||||
|
||||
const props = {
|
||||
label: t("repository.namespace"),
|
||||
helpText: t("help.namespaceHelpText"),
|
||||
@@ -90,7 +95,8 @@ const NamespaceAndNameFields: FC<Props> = ({ repository, onChange, setValid, dis
|
||||
onChange: handleNamespaceChange,
|
||||
errorMessage: t("validation.namespace-invalid"),
|
||||
validationError: namespaceValidationError,
|
||||
disabled: disabled
|
||||
disabled: disabled,
|
||||
informationMessage
|
||||
};
|
||||
|
||||
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
|
||||
|
||||
@@ -23,12 +23,13 @@
|
||||
*/
|
||||
|
||||
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 namespaceExceptionsRegex = /^(([0-9]{1,3})|(create)|(import))$/;
|
||||
|
||||
export const isNamespaceValid = (name: string) => {
|
||||
return nameRegex.test(name) && !namespaceExceptionsRegex.test(name);
|
||||
return isUserNameValid(name) && !namespaceExceptionsRegex.test(name);
|
||||
};
|
||||
|
||||
export const isNameValid = (name: string) => {
|
||||
|
||||
@@ -43,9 +43,12 @@ public class CustomNamespaceStrategy implements NamespaceStrategy {
|
||||
doThrow()
|
||||
.violation("invalid namespace", "namespace")
|
||||
.when(
|
||||
!ValidationUtil.isRepositoryNameValid(namespace)
|
||||
!ValidationUtil.isNameValid(namespace)
|
||||
|| ONE_TO_THREE_DIGITS.matcher(namespace).matches()
|
||||
|| namespace.equals("create"));
|
||||
|| namespace.equals("create")
|
||||
|| namespace.equals("import")
|
||||
|| namespace.equals("..")
|
||||
);
|
||||
|
||||
return namespace;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,6 @@ public class UsernameNamespaceStrategy implements NamespaceStrategy {
|
||||
|
||||
@Override
|
||||
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
|
||||
public ShiroRule shiro = new ShiroRule();
|
||||
|
||||
private RestDispatcher dispatcher = new RestDispatcher();
|
||||
private final RestDispatcher dispatcher = new RestDispatcher();
|
||||
|
||||
private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/"));
|
||||
|
||||
@@ -284,27 +284,6 @@ public class GroupRootResourceTest {
|
||||
|
||||
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\" }";
|
||||
request = MockHttpRequest
|
||||
.post("/" + GroupRootResource.GROUPS_PATH_V2)
|
||||
|
||||
@@ -63,4 +63,11 @@ class UsernameNamespaceStrategyTest {
|
||||
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