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:
Eduard Heimbuch
2021-03-25 09:59:23 +01:00
committed by GitHub
parent 8f2272885b
commit 22a0362892
20 changed files with 299 additions and 358 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}'`, () => {

View File

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

View File

@@ -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",

View File

@@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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