Fix artifact filename validation (#770)

* use validated ArtifactUpload object when creating a new artifact

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* rename method

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* add regular expression classes

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* add filename validation to UI upload button

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* move filename validation to uploadStarted

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* clean up code for UI error handling during artifact upload, assert
filename validation

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* update visibilities

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* clean up code

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* clean up code

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* change RegexChar class to enum and use i18n

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* typo, use StringBuilder

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* typo

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* use dedicated class for collections of regular expression characters

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* remove Optional, remove stringBuilder

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* PR findings

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* make regex validation method static

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>

* use WhiteListType.NONE for filename validation via mgmt-api

Signed-off-by: Stefan Klotz <stefan.klotz@bosch-si.com>
This commit is contained in:
Stefan Klotz
2018-12-17 10:17:46 +01:00
committed by Dominic Schabel
parent a2c1e5f132
commit 20d84a10eb
22 changed files with 536 additions and 225 deletions

View File

@@ -8,9 +8,10 @@
*/
package org.eclipse.hawkbit.repository;
import java.io.InputStream;
import java.util.Optional;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
@@ -23,6 +24,7 @@ import org.eclipse.hawkbit.repository.exception.EntityNotFoundException;
import org.eclipse.hawkbit.repository.exception.InvalidMD5HashException;
import org.eclipse.hawkbit.repository.exception.InvalidSHA1HashException;
import org.eclipse.hawkbit.repository.model.Artifact;
import org.eclipse.hawkbit.repository.model.ArtifactUpload;
import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -45,50 +47,8 @@ public interface ArtifactManagement {
* Persists artifact binary as provided by given InputStream. assign the
* artifact in addition to given {@link SoftwareModule}.
*
* @param inputStream
* to read from for artifact binary
* @param moduleId
* to assign the new artifact to
* @param filename
* of the artifact
* @param overrideExisting
* to <code>true</code> if the artifact binary can be overridden
* if it already exists
* @param filesize
* the size of the file in bytes.
*
* @return uploaded {@link Artifact}
*
* @throws ArtifactUploadFailedException
* if upload fails
* @throws EntityNotFoundException
* if given software module does not exist
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY)
Artifact create(@NotNull InputStream inputStream, long moduleId, final String filename,
final boolean overrideExisting, final long filesize);
/**
* Persists artifact binary as provided by given InputStream. assign the
* artifact in addition to given {@link SoftwareModule}.
*
* @param stream
* to read from for artifact binary
* @param moduleId
* to assign the new artifact to
* @param filename
* of the artifact
* @param providedSha1Sum
* optional sha1 checksum to check the new file against
* @param providedMd5Sum
* optional md5 checksum to check the new file against
* @param overrideExisting
* to <code>true</code> if the artifact binary can be overridden
* if it already exists
* @param contentType
* the contentType of the file
* @param filesize
* the size of the file in bytes.
* @param artifactUpload
* {@link ArtifactUpload} containing the upload information
*
* @return uploaded {@link Artifact}
*
@@ -102,10 +62,11 @@ public interface ArtifactManagement {
* if check against provided MD5 checksum failed
* @throws InvalidSHA1HashException
* if check against provided SHA1 checksum failed
* @throws ConstraintViolationException
* if {@link ArtifactUpload} contains invalid values
*/
@PreAuthorize(SpringEvalExpressions.HAS_AUTH_CREATE_REPOSITORY)
Artifact create(@NotNull InputStream stream, long moduleId, @NotEmpty String filename, String providedMd5Sum,
String providedSha1Sum, boolean overrideExisting, String contentType, long filesize);
Artifact create(@NotNull @Valid ArtifactUpload artifactUpload);
/**
* Garbage collects artifact binaries if only referenced by given

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Collection of regular expression characters to check strings
*/
public class RegexCharacterCollection {
private final EnumSet<RegexChar> characters;
private final Pattern findAnyCharacter;
public RegexCharacterCollection(final RegexChar... characters) {
this.characters = EnumSet.copyOf(Arrays.asList(characters));
this.findAnyCharacter = getPatternFindAnyCharacter();
}
public static boolean stringContainsCharacter(final String stringToCheck,
final RegexCharacterCollection regexCharacterCollection) {
return regexCharacterCollection.findAnyCharacter.matcher(stringToCheck).matches();
}
private Pattern getPatternFindAnyCharacter() {
final String regexCharacters = characters.stream().map(RegexChar::getRegExp)
.collect(Collectors.joining());
final String regularExpression = String.format(".*[%s]+.*", regexCharacters);
return Pattern.compile(regularExpression);
}
public enum RegexChar {
WHITESPACE("\\s", "character.whitespace"), DIGITS("0-9", "character.digits"), QUOTATION_MARKS("'\"",
"character.quotationMarks"), SLASHES("\\/\\\\", "character.slashes"), GREATER_THAN(
">"), LESS_THAN("<"), EQUALS_SYMBOL("="), EXCLAMATION_MARK("!"), QUESTION_MARK("?"), COLON(":");
private final String regExp;
private final String l18nReferenceDescription;
RegexChar(final String character) {
this(character, null);
}
RegexChar(final String regExp, final String l18nReferenceDescription) {
this.regExp = regExp;
this.l18nReferenceDescription = l18nReferenceDescription;
}
public String getRegExp() {
return regExp;
}
public Optional<String> getL18nReferenceDescription() {
return Optional.ofNullable(l18nReferenceDescription);
}
}
}

View File

@@ -0,0 +1,129 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository.model;
import java.io.InputStream;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.hibernate.validator.constraints.SafeHtml;
import org.hibernate.validator.constraints.SafeHtml.WhiteListType;
/**
* Use to create a new artifact.
*
*/
public class ArtifactUpload {
@NotNull
private final InputStream inputStream;
private final long moduleId;
@NotEmpty
@SafeHtml(whitelistType = WhiteListType.NONE, message = "Invalid characters in string")
private final String filename;
private final String providedMd5Sum;
private final String providedSha1Sum;
private final boolean overrideExisting;
private final String contentType;
private final long filesize;
/**
* Constructor
*
* @param inputStream
* to read from for artifact binary
* @param moduleId
* to assign the new artifact to
* @param filename
* of the artifact
* @param overrideExisting
* to <code>true</code> if the artifact binary can be overridden
* if it already exists
* @param filesize
* the size of the file in bytes.
*/
public ArtifactUpload(final InputStream inputStream, final long moduleId, final String filename,
final boolean overrideExisting, final long filesize) {
this(inputStream, moduleId, filename, null, null, overrideExisting, null, filesize);
}
/**
* Constructor
*
* @param inputStream
* to read from for artifact binary
* @param moduleId
* to assign the new artifact to
* @param filename
* of the artifact
* @param providedSha1Sum
* optional sha1 checksum to check the new file against
* @param providedMd5Sum
* optional md5 checksum to check the new file against
* @param overrideExisting
* to <code>true</code> if the artifact binary can be overridden
* if it already exists
* @param contentType
* the contentType of the file
* @param filesize
* the size of the file in bytes.
*/
public ArtifactUpload(final InputStream inputStream, final long moduleId, final String filename,
final String providedMd5Sum, final String providedSha1Sum, final boolean overrideExisting,
final String contentType, final long filesize) {
this.inputStream = inputStream;
this.moduleId = moduleId;
this.filename = filename;
this.providedMd5Sum = providedMd5Sum;
this.providedSha1Sum = providedSha1Sum;
this.overrideExisting = overrideExisting;
this.contentType = contentType;
this.filesize = filesize;
}
public InputStream getInputStream() {
return inputStream;
}
public long getModuleId() {
return moduleId;
}
public String getFilename() {
return filename;
}
public String getProvidedMd5Sum() {
return providedMd5Sum;
}
public String getProvidedSha1Sum() {
return providedSha1Sum;
}
public boolean overrideExisting() {
return overrideExisting;
}
public String getContentType() {
return contentType;
}
public long getFilesize() {
return filesize;
}
}

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.hawkbit.repository;
import static org.assertj.core.api.Assertions.assertThat;
import org.eclipse.hawkbit.repository.RegexCharacterCollection.RegexChar;
import org.junit.Test;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.qameta.allure.Story;
@Feature("Unit Tests - Repository")
@Story("Regular expression helper")
public class RegexCharTest {
private static final String TEST_STRING = getPrintableAsciiCharacters();
private static final int INDEX_FIRST_PRINTABLE_ASCII_CHAR = 32;
private static final int INDEX_LAST_PRINTABLE_ASCII_CHAR = 127;
@Test
@Description("Verifies every RegexChar can be used to exclusively find the desired characters in a String.")
public void allRegexCharsOnlyFindExpectedChars() {
for (final RegexChar character : RegexChar.values()) {
switch (character) {
case DIGITS:
assertRegexCharExclusivelyFindsGivenCharacters(character, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
break;
case WHITESPACE:
assertRegexCharExclusivelyFindsGivenCharacters(character, " ", "\t");
break;
case SLASHES:
assertRegexCharExclusivelyFindsGivenCharacters(character, "/", "\\");
break;
case QUOTATION_MARKS:
assertRegexCharExclusivelyFindsGivenCharacters(character, "\"", "'");
break;
default:
assertRegexCharExclusivelyFindsGivenCharacters(character, character.getRegExp());
break;
}
}
}
@Test
@Description("Verifies that combinations of RegexChars can be used to find the desired characters in a String.")
public void combinedRegexCharsFindExpectedChars() {
final RegexCharacterCollection greaterAndLessThan = new RegexCharacterCollection(RegexChar.GREATER_THAN,
RegexChar.LESS_THAN);
final RegexCharacterCollection equalsAndQuestionMark = new RegexCharacterCollection(RegexChar.EQUALS_SYMBOL,
RegexChar.QUESTION_MARK);
final RegexCharacterCollection colonAndWhitespace = new RegexCharacterCollection(RegexChar.COLON,
RegexChar.WHITESPACE);
assertRegexCharsExclusivelyFindsGivenCharacters(greaterAndLessThan, "<", ">");
assertRegexCharsExclusivelyFindsGivenCharacters(equalsAndQuestionMark, "=", "?");
assertRegexCharsExclusivelyFindsGivenCharacters(colonAndWhitespace, ":", " ", "\t");
}
private void assertRegexCharExclusivelyFindsGivenCharacters(final RegexChar characterToVerify,
final String... charactersExpectedToBeFoundByRegex) {
assertRegexCharsExclusivelyFindsGivenCharacters(new RegexCharacterCollection(characterToVerify),
charactersExpectedToBeFoundByRegex);
}
private void assertRegexCharsExclusivelyFindsGivenCharacters(final RegexCharacterCollection regexToVerify,
final String... charactersExpectedToBeFoundByRegex) {
String notMatchingString = TEST_STRING;
for(final String character : charactersExpectedToBeFoundByRegex) {
notMatchingString = notMatchingString.replace(character, "");
}
for(final String character : charactersExpectedToBeFoundByRegex) {
assertThat(RegexCharacterCollection.stringContainsCharacter("", regexToVerify)).isFalse();
assertThat(RegexCharacterCollection.stringContainsCharacter(notMatchingString, regexToVerify)).isFalse();
assertThat(RegexCharacterCollection.stringContainsCharacter(character, regexToVerify)).isTrue();
assertThat(RegexCharacterCollection
.stringContainsCharacter(insertStringIntoString(notMatchingString, character, 0), regexToVerify))
.isTrue();
assertThat(RegexCharacterCollection.stringContainsCharacter(
insertStringIntoString(notMatchingString, character, notMatchingString.length()), regexToVerify)).isTrue();
assertThat(RegexCharacterCollection.stringContainsCharacter(
insertStringIntoString(notMatchingString, character, notMatchingString.length() / 2), regexToVerify)).isTrue();
}
}
private static String getPrintableAsciiCharacters() {
final StringBuilder stringBuilder = new StringBuilder();
for (int i = INDEX_FIRST_PRINTABLE_ASCII_CHAR; i < INDEX_LAST_PRINTABLE_ASCII_CHAR; i++) {
stringBuilder.append((char) i);
}
stringBuilder.append("\t");
return stringBuilder.toString();
}
private static String insertStringIntoString(final String baseString, final String stringToInsert,
final int position) {
final StringBuilder stringBuilder = new StringBuilder(baseString);
return stringBuilder.insert(position, stringToInsert).toString();
}
}