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
public boolean isValid() {
return ValidationUtil.isRepositoryNameValid(namespace)
return ValidationUtil.isNameValid(namespace)
&& ValidationUtil.isRepositoryNameValid(name)
&& Util.isNotEmpty(type)
&& ((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.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 {

View File

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

View File

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

View File

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

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=""
/>
</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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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