implement password-hashing (not fully integrated yet) #67
@ -66,6 +66,7 @@ dependencies {
|
|||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
||||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
implementation 'org.apache.commons:commons-text:1.11.0'
|
||||||
|
implementation 'commons-codec:commons-codec:1.17.0'
|
||||||
implementation 'org.modelmapper:modelmapper:3.2.0'
|
implementation 'org.modelmapper:modelmapper:3.2.0'
|
||||||
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
||||||
|
107
src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java
Normal file
107
src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
|
||||||
|
|
||||||
|
public class HashProcessor {
|
||||||
|
|
||||||
|
private static final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
|
public enum Algorithm {
|
||||||
|
SHA512
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Base64.Encoder BASE64 = Base64.getEncoder();
|
||||||
|
private static final String SALT_CHARACTERS =
|
||||||
|
"abcdefghijklmnopqrstuvwxyz" +
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||||
|
"0123456789$_";
|
||||||
|
|
||||||
|
private final MessageDigest generator;
|
||||||
|
private byte[] saltBytes;
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static HashProcessor hashAlgorithm(final Algorithm algorithm) {
|
||||||
|
return new HashProcessor(algorithm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashProcessor(final Algorithm algorithm) throws NoSuchAlgorithmException {
|
||||||
|
generator = MessageDigest.getInstance(algorithm.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String generate(final String password) {
|
||||||
|
final byte[] saltedPasswordDigest = calculateSaltedDigest(password);
|
||||||
|
final byte[] hashBytes = appendSaltToSaltedDigest(saltedPasswordDigest);
|
||||||
|
return BASE64.encodeToString(hashBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] appendSaltToSaltedDigest(final byte[] saltedPasswordDigest) {
|
||||||
|
final byte[] hashBytes = new byte[saltedPasswordDigest.length + 1 + saltBytes.length];
|
||||||
|
System.arraycopy(saltedPasswordDigest, 0, hashBytes, 0, saltedPasswordDigest.length);
|
||||||
|
hashBytes[saltedPasswordDigest.length] = ':';
|
||||||
|
System.arraycopy(saltBytes, 0, hashBytes, saltedPasswordDigest.length+1, saltBytes.length);
|
||||||
|
return hashBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] calculateSaltedDigest(final String password) {
|
||||||
|
generator.reset();
|
||||||
|
generator.update(password.getBytes());
|
||||||
|
generator.update(saltBytes);
|
||||||
|
return generator.digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashProcessor withSalt(final byte[] saltBytes) {
|
||||||
|
this.saltBytes = saltBytes;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashProcessor withSalt(final String salt) {
|
||||||
|
return withSalt(salt.getBytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashProcessor withRandomSalt() {
|
||||||
|
final var stringBuilder = new StringBuilder(16);
|
||||||
|
for (int i = 0; i < 16; ++i) {
|
||||||
|
int randomIndex = secureRandom.nextInt(SALT_CHARACTERS.length());
|
||||||
|
stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex));
|
||||||
|
}
|
||||||
|
return withSalt(stringBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashVerifier withHash(final String hash) {
|
||||||
|
return new HashVerifier(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getLastPart(String input, char delimiter) {
|
||||||
|
final var lastIndex = input.lastIndexOf(delimiter);
|
||||||
|
if (lastIndex == -1) {
|
||||||
|
throw new IllegalArgumentException("cannot determine salt, expected: 'digest:salt', but no ':' found");
|
||||||
|
}
|
||||||
|
return input.substring(lastIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HashVerifier {
|
||||||
|
|
||||||
|
private final String hash;
|
||||||
|
|
||||||
|
public HashVerifier(final String hash) {
|
||||||
|
this.hash = hash;
|
||||||
|
withSalt(getLastPart(new String(Base64.getDecoder().decode(hash)), ':'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void verify(String password) {
|
||||||
|
final var computedHash = hashAlgorithm(SHA512).withSalt(saltBytes).generate(password);
|
||||||
|
if ( !computedHash.equals(hash) ) {
|
||||||
|
throw new ValidationException("invalid password");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -16,7 +16,7 @@ import static java.util.Optional.ofNullable;
|
|||||||
|
|
||||||
public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
|
public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
|
||||||
|
|
||||||
public HsBookingItemEntityValidator(final ValidatableProperty<?>... properties) {
|
public HsBookingItemEntityValidator(final ValidatableProperty<?, ?>... properties) {
|
||||||
super(properties);
|
super(properties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +54,7 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
|||||||
// TODO.refa: convert into generic shape like multi-options validator
|
// TODO.refa: convert into generic shape like multi-options validator
|
||||||
private static String validateMaxTotalValue(
|
private static String validateMaxTotalValue(
|
||||||
final HsBookingItemEntity bookingItem,
|
final HsBookingItemEntity bookingItem,
|
||||||
final ValidatableProperty<?> propDef) {
|
final ValidatableProperty<?, ?> propDef) {
|
||||||
final var propName = propDef.propertyName();
|
final var propName = propDef.propertyName();
|
||||||
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
|
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
|
||||||
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
|
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
|
||||||
|
@ -24,7 +24,7 @@ import static java.util.Optional.ofNullable;
|
|||||||
|
|
||||||
public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
|
public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
|
||||||
|
|
||||||
static final ValidatableProperty<?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?>[0];
|
static final ValidatableProperty<?, ?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?, ?>[0];
|
||||||
|
|
||||||
private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation;
|
private final HsHostingAssetEntityValidator.BookingItem bookingItemValidation;
|
||||||
private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation;
|
private final HsHostingAssetEntityValidator.ParentAsset parentAssetValidation;
|
||||||
@ -36,7 +36,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
|||||||
@NotNull final ParentAsset parentAssetValidation,
|
@NotNull final ParentAsset parentAssetValidation,
|
||||||
@NotNull final AssignedToAsset assignedToAssetValidation,
|
@NotNull final AssignedToAsset assignedToAssetValidation,
|
||||||
@NotNull final AlarmContact alarmContactValidation,
|
@NotNull final AlarmContact alarmContactValidation,
|
||||||
final ValidatableProperty<?>... properties) {
|
final ValidatableProperty<?, ?>... properties) {
|
||||||
super(properties);
|
super(properties);
|
||||||
this.bookingItemValidation = bookingItemValidation;
|
this.bookingItemValidation = bookingItemValidation;
|
||||||
this.parentAssetValidation = parentAssetValidation;
|
this.parentAssetValidation = parentAssetValidation;
|
||||||
@ -105,7 +105,7 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
|||||||
// TODO.test: check, if there are any hosting assets which need this validation at all
|
// TODO.test: check, if there are any hosting assets which need this validation at all
|
||||||
private String validateMaxTotalValue(
|
private String validateMaxTotalValue(
|
||||||
final HsHostingAssetEntity hostingAsset,
|
final HsHostingAssetEntity hostingAsset,
|
||||||
final ValidatableProperty<?> propDef) {
|
final ValidatableProperty<?, ?> propDef) {
|
||||||
final var propName = propDef.propertyName();
|
final var propName = propDef.propertyName();
|
||||||
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
|
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
|
||||||
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
|
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hash.HashProcessor;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
@ -30,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator {
|
|||||||
.withDefault("/bin/false"),
|
.withDefault("/bin/false"),
|
||||||
stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir),
|
stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir),
|
||||||
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(),
|
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(),
|
||||||
passwordProperty("password").minLength(8).maxLength(40).writeOnly());
|
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -9,7 +9,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class BooleanProperty extends ValidatableProperty<Boolean> {
|
public class BooleanProperty extends ValidatableProperty<BooleanProperty, Boolean> {
|
||||||
|
|
||||||
private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL);
|
private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL);
|
||||||
|
|
||||||
@ -23,7 +23,7 @@ public class BooleanProperty extends ValidatableProperty<Boolean> {
|
|||||||
return new BooleanProperty(propertyName);
|
return new BooleanProperty(propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
|
public BooleanProperty falseIf(final String refPropertyName, final String refPropertyValue) {
|
||||||
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
|
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import java.util.List;
|
|||||||
import static java.util.Arrays.stream;
|
import static java.util.Arrays.stream;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class EnumerationProperty extends ValidatableProperty<String> {
|
public class EnumerationProperty extends ValidatableProperty<EnumerationProperty, String> {
|
||||||
|
|
||||||
private static final String[] KEY_ORDER = Array.join(
|
private static final String[] KEY_ORDER = Array.join(
|
||||||
ValidatableProperty.KEY_ORDER_HEAD,
|
ValidatableProperty.KEY_ORDER_HEAD,
|
||||||
@ -26,12 +26,12 @@ public class EnumerationProperty extends ValidatableProperty<String> {
|
|||||||
return new EnumerationProperty(propertyName);
|
return new EnumerationProperty(propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<String> values(final String... values) {
|
public EnumerationProperty values(final String... values) {
|
||||||
this.values = values;
|
this.values = values;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
|
||||||
if (hasDeferredInit()) {
|
if (hasDeferredInit()) {
|
||||||
if (this.values != null) {
|
if (this.values != null) {
|
||||||
throw new IllegalStateException("property " + this + " already has values");
|
throw new IllegalStateException("property " + this + " already has values");
|
||||||
@ -40,8 +40,8 @@ public class EnumerationProperty extends ValidatableProperty<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<String> valuesFromProperties(final String propertyNamePrefix) {
|
public EnumerationProperty valuesFromProperties(final String propertyNamePrefix) {
|
||||||
this.setDeferredInit( (ValidatableProperty<?>[] allProperties) -> stream(allProperties)
|
this.setDeferredInit( (ValidatableProperty<?, ?>[] allProperties) -> stream(allProperties)
|
||||||
.map(ValidatableProperty::propertyName)
|
.map(ValidatableProperty::propertyName)
|
||||||
.filter(name -> name.startsWith(propertyNamePrefix))
|
.filter(name -> name.startsWith(propertyNamePrefix))
|
||||||
.map(name -> name.substring(propertyNamePrefix.length()))
|
.map(name -> name.substring(propertyNamePrefix.length()))
|
||||||
|
@ -14,9 +14,9 @@ import static java.util.Collections.emptyList;
|
|||||||
// TODO.refa: rename to HsEntityProcessor, also subclasses
|
// TODO.refa: rename to HsEntityProcessor, also subclasses
|
||||||
public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||||
|
|
||||||
public final ValidatableProperty<?>[] propertyValidators;
|
public final ValidatableProperty<?, ?>[] propertyValidators;
|
||||||
|
|
||||||
public HsEntityValidator(final ValidatableProperty<?>... validators) {
|
public HsEntityValidator(final ValidatableProperty<?, ?>... validators) {
|
||||||
propertyValidators = validators;
|
propertyValidators = validators;
|
||||||
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
|
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
|
||||||
}
|
}
|
||||||
@ -68,7 +68,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
|||||||
.orElse(emptyList()));
|
.orElse(emptyList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?> prop, final Map<String, Object> propValues) {
|
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?, ?> prop, final Map<String, Object> propValues) {
|
||||||
final var value = prop.getValue(propValues);
|
final var value = prop.getValue(propValues);
|
||||||
if (value instanceof Integer) {
|
if (value instanceof Integer) {
|
||||||
return (Integer) value;
|
return (Integer) value;
|
||||||
@ -92,10 +92,10 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
|||||||
public Map<String, Object> postProcess(final E entity, final Map<String, Object> config) {
|
public Map<String, Object> postProcess(final E entity, final Map<String, Object> config) {
|
||||||
final var copy = new HashMap<>(config);
|
final var copy = new HashMap<>(config);
|
||||||
stream(propertyValidators).forEach(p -> {
|
stream(propertyValidators).forEach(p -> {
|
||||||
|
// FIXME: maybe move to ValidatableProperty.postProcess(...)?
|
||||||
if ( p.isWriteOnly()) {
|
if ( p.isWriteOnly()) {
|
||||||
copy.remove(p.propertyName);
|
copy.remove(p.propertyName);
|
||||||
}
|
} else if (p.isComputed()) {
|
||||||
if (p.isComputed()) {
|
|
||||||
copy.put(p.propertyName, p.compute(entity));
|
copy.put(p.propertyName, p.compute(entity));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import org.apache.commons.lang3.Validate;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class IntegerProperty extends ValidatableProperty<Integer> {
|
public class IntegerProperty extends ValidatableProperty<IntegerProperty, Integer> {
|
||||||
|
|
||||||
private final static String[] KEY_ORDER = Array.join(
|
private final static String[] KEY_ORDER = Array.join(
|
||||||
ValidatableProperty.KEY_ORDER_HEAD,
|
ValidatableProperty.KEY_ORDER_HEAD,
|
||||||
@ -30,7 +30,7 @@ public class IntegerProperty extends ValidatableProperty<Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
|
||||||
Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given");
|
Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given");
|
||||||
Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
|
Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,24 @@
|
|||||||
package net.hostsharing.hsadminng.hs.validation;
|
package net.hostsharing.hsadminng.hs.validation;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm;
|
||||||
|
import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
public class PasswordProperty extends StringProperty {
|
public class PasswordProperty extends StringProperty<PasswordProperty> {
|
||||||
|
|
||||||
|
private static final String[] KEY_ORDER = insertAfterEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing");
|
||||||
|
|
||||||
|
private Algorithm hashedUsing;
|
||||||
|
|
||||||
private PasswordProperty(final String propertyName) {
|
private PasswordProperty(final String propertyName) {
|
||||||
super(propertyName);
|
super(propertyName, KEY_ORDER);
|
||||||
undisclosed();
|
undisclosed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,7 +32,15 @@ public class PasswordProperty extends StringProperty {
|
|||||||
validatePassword(result, propValue);
|
validatePassword(result, propValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO.impl: only a SHA512 hash should be stored in the database, not the password itself
|
public PasswordProperty hashedUsing(final Algorithm algorithm) {
|
||||||
|
this.hashedUsing = algorithm;
|
||||||
|
// FIXME: computedBy is too late, we need preprocess
|
||||||
|
computedBy((entity)
|
||||||
|
-> ofNullable(entity.getDirectValue(propertyName, String.class))
|
||||||
|
.map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password))
|
||||||
|
.orElse(null));
|
||||||
|
return self();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String simpleTypeName() {
|
protected String simpleTypeName() {
|
||||||
@ -60,6 +77,5 @@ public class PasswordProperty extends StringProperty {
|
|||||||
if (containsColon) {
|
if (containsColon) {
|
||||||
result.add(propertyName + "' must not contain colon (':')");
|
result.add(propertyName + "' must not contain colon (':')");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,10 +6,11 @@ import net.hostsharing.hsadminng.mapper.Array;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@Setter
|
|
||||||
public class StringProperty extends ValidatableProperty<String> {
|
|
||||||
|
|
||||||
private static final String[] KEY_ORDER = Array.join(
|
@Setter
|
||||||
|
public class StringProperty<P extends StringProperty<P>> extends ValidatableProperty<P, String> {
|
||||||
|
|
||||||
|
protected static final String[] KEY_ORDER = Array.join(
|
||||||
ValidatableProperty.KEY_ORDER_HEAD,
|
ValidatableProperty.KEY_ORDER_HEAD,
|
||||||
Array.of("matchesRegEx", "minLength", "maxLength"),
|
Array.of("matchesRegEx", "minLength", "maxLength"),
|
||||||
ValidatableProperty.KEY_ORDER_TAIL,
|
ValidatableProperty.KEY_ORDER_TAIL,
|
||||||
@ -23,23 +24,27 @@ public class StringProperty extends ValidatableProperty<String> {
|
|||||||
super(String.class, propertyName, KEY_ORDER);
|
super(String.class, propertyName, KEY_ORDER);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StringProperty stringProperty(final String propertyName) {
|
protected StringProperty(final String propertyName, final String[] keyOrder) {
|
||||||
return new StringProperty(propertyName);
|
super(String.class, propertyName, keyOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty minLength(final int minLength) {
|
public static StringProperty<?> stringProperty(final String propertyName) {
|
||||||
|
return new StringProperty<>(propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public P minLength(final int minLength) {
|
||||||
this.minLength = minLength;
|
this.minLength = minLength;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty maxLength(final int maxLength) {
|
public P maxLength(final int maxLength) {
|
||||||
this.maxLength = maxLength;
|
this.maxLength = maxLength;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public StringProperty matchesRegEx(final String regExPattern) {
|
public P matchesRegEx(final String regExPattern) {
|
||||||
this.matchesRegEx = Pattern.compile(regExPattern);
|
this.matchesRegEx = Pattern.compile(regExPattern);
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -47,9 +52,9 @@ public class StringProperty extends ValidatableProperty<String> {
|
|||||||
*
|
*
|
||||||
* @return this;
|
* @return this;
|
||||||
*/
|
*/
|
||||||
public StringProperty undisclosed() {
|
public P undisclosed() {
|
||||||
this.undisclosed = true;
|
this.undisclosed = true;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -25,7 +25,7 @@ import static java.util.Optional.ofNullable;
|
|||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public abstract class ValidatableProperty<T> {
|
public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
|
||||||
|
|
||||||
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
|
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
|
||||||
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
|
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
|
||||||
@ -51,7 +51,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
@Accessors(makeFinal = true, chain = true, fluent = false)
|
@Accessors(makeFinal = true, chain = true, fluent = false)
|
||||||
private boolean writeOnly;
|
private boolean writeOnly;
|
||||||
|
|
||||||
private Function<ValidatableProperty<?>[], T[]> deferredInit;
|
private Function<ValidatableProperty<?, ?>[], T[]> deferredInit;
|
||||||
private boolean isTotalsValidator = false;
|
private boolean isTotalsValidator = false;
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@ -59,11 +59,16 @@ public abstract class ValidatableProperty<T> {
|
|||||||
|
|
||||||
private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty
|
private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty
|
||||||
|
|
||||||
|
public final P self() {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (P) this;
|
||||||
|
}
|
||||||
|
|
||||||
public String unit() {
|
public String unit() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setDeferredInit(final Function<ValidatableProperty<?>[], T[]> function) {
|
protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]> function) {
|
||||||
this.deferredInit = function;
|
this.deferredInit = function;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,47 +76,47 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return deferredInit != null;
|
return deferredInit != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T[] doDeferredInit(final ValidatableProperty<?>[] allProperties) {
|
public T[] doDeferredInit(final ValidatableProperty<?, ?>[] allProperties) {
|
||||||
return deferredInit.apply(allProperties);
|
return deferredInit.apply(allProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> writeOnly() {
|
public P writeOnly() {
|
||||||
this.writeOnly = true;
|
this.writeOnly = true;
|
||||||
optional();
|
optional();
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> readOnly() {
|
public P readOnly() {
|
||||||
this.readOnly = true;
|
this.readOnly = true;
|
||||||
optional();
|
optional();
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> required() {
|
public P required() {
|
||||||
required = TRUE;
|
required = TRUE;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> optional() {
|
public ValidatableProperty<P, T> optional() {
|
||||||
required = FALSE;
|
required = FALSE;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> withDefault(final T value) {
|
public P withDefault(final T value) {
|
||||||
defaultValue = value;
|
defaultValue = value;
|
||||||
required = FALSE;
|
required = FALSE;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deferredInit(final ValidatableProperty<?>[] allProperties) {
|
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> asTotalLimit() {
|
public P asTotalLimit() {
|
||||||
isTotalsValidator = true;
|
isTotalsValidator = true;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> asTotalLimitFor(final String propertyName, final String propertyValue) {
|
public P asTotalLimitFor(final String propertyName, final String propertyValue) {
|
||||||
if (asTotalLimitValidators == null) {
|
if (asTotalLimitValidators == null) {
|
||||||
asTotalLimitValidators = new ArrayList<>();
|
asTotalLimitValidators = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@ -132,7 +137,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return emptyList();
|
return emptyList();
|
||||||
};
|
};
|
||||||
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
|
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String propertyName() {
|
public String propertyName() {
|
||||||
@ -147,7 +152,7 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return thresholdPercentage;
|
return thresholdPercentage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) {
|
public ValidatableProperty<P, T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) {
|
||||||
if (asTotalLimitValidators == null) {
|
if (asTotalLimitValidators == null) {
|
||||||
asTotalLimitValidators = new ArrayList<>();
|
asTotalLimitValidators = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@ -155,9 +160,9 @@ public abstract class ValidatableProperty<T> {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<?> withThreshold(final Integer percentage) {
|
public P withThreshold(final Integer percentage) {
|
||||||
this.thresholdPercentage = percentage;
|
this.thresholdPercentage = percentage;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public final List<String> validate(final PropertiesProvider propsProvider) {
|
public final List<String> validate(final PropertiesProvider propsProvider) {
|
||||||
@ -250,10 +255,10 @@ public abstract class ValidatableProperty<T> {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValidatableProperty<T> computedBy(final Function<PropertiesProvider, T> compute) {
|
public P computedBy(final Function<PropertiesProvider, T> compute) {
|
||||||
this.computedBy = compute;
|
this.computedBy = compute;
|
||||||
this.computed = true;
|
this.computed = true;
|
||||||
return this;
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
public <E extends PropertiesProvider> T compute(final E entity) {
|
public <E extends PropertiesProvider> T compute(final E entity) {
|
||||||
|
@ -6,6 +6,8 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter,
|
* Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter,
|
||||||
* but no Array.of(...). Here it is.
|
* but no Array.of(...). Here it is.
|
||||||
@ -48,4 +50,17 @@ public class Array {
|
|||||||
public static <T> T[] emptyArray() {
|
public static <T> T[] emptyArray() {
|
||||||
return of();
|
return of();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> T[] insertAfterEntry(final T[] array, final T entryToFind, final T newEntry) {
|
||||||
|
final var arrayList = new ArrayList<>(asList(array));
|
||||||
|
final var index = arrayList.indexOf(entryToFind);
|
||||||
|
if (index < 0) {
|
||||||
|
throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array));
|
||||||
|
}
|
||||||
|
arrayList.add(index + 1, newEntry);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length);
|
||||||
|
return arrayList.toArray(extendedArray);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.hostsharing.hsadminng.hash;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
|
class HashProcessorUnitTest {
|
||||||
|
|
||||||
|
final String OTHER_PASSWORD = "other password";
|
||||||
|
final String GIVEN_PASSWORD = "given password";
|
||||||
|
final String GIVEN_PASSWORD_HASH = "foKDNQP0oZo0pjFpss5vNl0kfHOs6MKMaJUUbpJTg6hqI1WY+KbU/PKQIg2xt/mwDMmW5WR0pdUZnTv8RPTfhjprZUNqTXJsUXczQnczYUxE";
|
||||||
|
final String GIVEN_SALT = "given salt";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verifiesHashedPasswordWithRandomSalt() {
|
||||||
|
final var hash = hashAlgorithm(SHA512).withRandomSalt().generate(GIVEN_PASSWORD);
|
||||||
|
hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void verifiesHashedPasswordWithGivenSalt() {
|
||||||
|
final var hash = hashAlgorithm(SHA512).withSalt(GIVEN_SALT).generate(GIVEN_PASSWORD);
|
||||||
|
|
||||||
|
final var decoded = new String(Base64.getDecoder().decode(hash));
|
||||||
|
assertThat(decoded).endsWith(":" + GIVEN_SALT);
|
||||||
|
hashAlgorithm(SHA512).withHash(hash).verify(GIVEN_PASSWORD); // throws exception if wrong
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void throwsExceptionForInvalidPassword() {
|
||||||
|
final var throwable = catchThrowable(() ->
|
||||||
|
hashAlgorithm(SHA512).withHash(GIVEN_PASSWORD_HASH).verify(OTHER_PASSWORD));
|
||||||
|
|
||||||
|
assertThat(throwable).hasMessage("invalid password");
|
||||||
|
}
|
||||||
|
}
|
@ -125,7 +125,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
|
|||||||
"{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}",
|
"{type=enumeration, propertyName=shell, values=[/bin/false, /bin/bash, /bin/csh, /bin/dash, /usr/bin/tcsh, /usr/bin/zsh, /usr/bin/passwd], defaultValue=/bin/false}",
|
||||||
"{type=string, propertyName=homedir, readOnly=true, computed=true}",
|
"{type=string, propertyName=homedir, readOnly=true, computed=true}",
|
||||||
"{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}",
|
"{type=string, propertyName=totpKey, matchesRegEx=^0x([0-9A-Fa-f]{2})+$, minLength=20, maxLength=256, writeOnly=true, undisclosed=true}",
|
||||||
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, undisclosed=true}"
|
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, hashedUsing=SHA512, undisclosed=true}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,13 +6,18 @@ import org.junit.jupiter.params.provider.ValueSource;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512;
|
||||||
|
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
|
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
|
||||||
|
import static net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class PasswordPropertyUnitTest {
|
class PasswordPropertyUnitTest {
|
||||||
|
|
||||||
private final ValidatableProperty<String> passwordProp = passwordProperty("password").minLength(8).maxLength(40).writeOnly();
|
private final ValidatableProperty<PasswordProperty, String> passwordProp =
|
||||||
|
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(SHA512).writeOnly();
|
||||||
private final List<String> violations = new ArrayList<>();
|
private final List<String> violations = new ArrayList<>();
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ -89,4 +94,27 @@ class PasswordPropertyUnitTest {
|
|||||||
.contains("password' must not contain colon (':')")
|
.contains("password' must not contain colon (':')")
|
||||||
.doesNotContain(givenPassword);
|
.doesNotContain(givenPassword);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldComputeHash() {
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var result = passwordProp.compute(new PropertiesProvider() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> directProps() {
|
||||||
|
return Map.ofEntries(
|
||||||
|
entry(passwordProp.propertyName, "some password")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object getContextValue(final String propName) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// then
|
||||||
|
hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user