integrate-sha512-password-hashing (#68)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #68
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-07-01 15:53:50 +02:00
parent 3391ec6cc9
commit 409f5e97c7
29 changed files with 419 additions and 341 deletions

View File

@ -66,7 +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 'net.java.dev.jna:jna:5.8.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'

View File

@ -15,7 +15,7 @@ public class MultiValidationException extends ValidationException {
); );
} }
public static void throwInvalid(final List<String> violations) { public static void throwIfNotEmpty(final List<String> violations) {
if (!violations.isEmpty()) { if (!violations.isEmpty()) {
throw new MultiValidationException(violations); throw new MultiValidationException(violations);
} }

View File

@ -1,107 +0,0 @@
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");
}
}
}
}

View File

@ -0,0 +1,112 @@
package net.hostsharing.hsadminng.hash;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.random.RandomGenerator;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class LinuxEtcShadowHashGenerator {
private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int SALT_LENGTH = 16;
private final String plaintextPassword;
private Algorithm algorithm;
public enum Algorithm {
SHA512("6"),
YESCRYPT("y");
final String prefix;
Algorithm(final String prefix) {
this.prefix = prefix;
}
static Algorithm byPrefix(final String prefix) {
return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny()
.orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'"));
}
}
private static final String SALT_CHARACTERS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
private String salt;
public static LinuxEtcShadowHashGenerator hash(final String plaintextPassword) {
return new LinuxEtcShadowHashGenerator(plaintextPassword);
}
private LinuxEtcShadowHashGenerator(final String plaintextPassword) {
this.plaintextPassword = plaintextPassword;
}
public LinuxEtcShadowHashGenerator using(final Algorithm algorithm) {
this.algorithm = algorithm;
return this;
}
void verify(final String givenHash) {
final var parts = givenHash.split("\\$");
if (parts.length < 3 || parts.length > 5) {
throw new IllegalArgumentException("not a " + algorithm.name() + " Linux hash: " + givenHash);
}
algorithm = Algorithm.byPrefix(parts[1]);
salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
if (!generate().equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
}
public String generate() {
if (salt == null) {
throw new IllegalStateException("no salt given");
}
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
}
return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public LinuxEtcShadowHashGenerator withSalt(final String salt) {
this.salt = salt;
return this;
}
public LinuxEtcShadowHashGenerator withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
final var stringBuilder = new StringBuilder(SALT_LENGTH);
for (int i = 0; i < SALT_LENGTH; ++i) {
int randomIndex = random.nextInt(SALT_CHARACTERS.length());
stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex));
}
return withSalt(stringBuilder.toString());
}
public static void main(String[] args) {
System.out.println(NativeCryptLibrary.INSTANCE.crypt("given password", "$6$abcdefghijklmno"));
}
public interface NativeCryptLibrary extends Library {
NativeCryptLibrary INSTANCE = Native.load("crypt", NativeCryptLibrary.class);
String crypt(String password, String salt);
}
}

View File

@ -20,22 +20,23 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
super(properties); super(properties);
} }
public List<String> validate(final HsBookingItemEntity bookingItem) { @Override
public List<String> validateEntity(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
}
@Override
public List<String> validateContext(final HsBookingItemEntity bookingItem) {
return sequentiallyValidate( return sequentiallyValidate(
() -> validateProperties(bookingItem),
() -> optionallyValidate(bookingItem.getParentItem()), () -> optionallyValidate(bookingItem.getParentItem()),
() -> validateAgainstSubEntities(bookingItem) () -> validateAgainstSubEntities(bookingItem)
); );
} }
private List<String> validateProperties(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) { private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null return bookingItem != null
? enrich(prefix(bookingItem.toShortString(), ""), ? enrich(prefix(bookingItem.toShortString(), ""),
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
: emptyList(); : emptyList();
} }

View File

@ -45,11 +45,13 @@ public class HsBookingItemEntityValidatorRegistry {
} }
public static List<String> doValidate(final HsBookingItemEntity bookingItem) { public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
return HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validate(bookingItem); return HsEntityValidator.sequentiallyValidate(
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem));
} }
public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) { public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) {
MultiValidationException.throwInvalid(doValidate(entityToSave)); MultiValidationException.throwIfNotEmpty(doValidate(entityToSave));
return entityToSave; return entityToSave;
} }
} }

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.hs.hosting.asset; package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityProcessor;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry; import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
@ -21,11 +22,10 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated;
@RestController @RestController
public class HsHostingAssetController implements HsHostingAssetsApi { public class HsHostingAssetController implements HsHostingAssetsApi {
@ -56,7 +56,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final var entities = assetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type)); final var entities = assetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type));
final var resources = mapper.mapList(entities, HsHostingAssetResource.class); final var resources = mapper.mapList(entities, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources); return ResponseEntity.ok(resources);
} }
@ -70,16 +70,21 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = validated(assetRepo.save(entityToSave)); final var mapped = new HsHostingAssetEntityProcessor(entity)
.validateEntity()
.prepareForSave()
.saveUsing(assetRepo::save)
.validateContext()
.mapUsing(e -> mapper.map(e, HsHostingAssetResource.class))
.revampProperties();
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/hosting/assets/{id}") .path("/api/hs/hosting/assets/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(mapped.getUuid())
.toUri(); .toUri();
final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.created(uri).body(mapped); return ResponseEntity.created(uri).body(mapped);
} }
@ -123,21 +128,18 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
context.define(currentUser, assumedRoles); context.define(currentUser, assumedRoles);
final var current = assetRepo.findByUuid(assetUuid).orElseThrow(); final var entity = assetRepo.findByUuid(assetUuid).orElseThrow();
new HsHostingAssetEntityPatcher(em, current).apply(body); new HsHostingAssetEntityPatcher(em, entity).apply(body);
// TODO.refa: draft for an alternative API final var mapped = new HsHostingAssetEntityProcessor(entity)
// validate(current) // self-validation, hashing passwords etc. .validateEntity()
// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. .prepareForSave()
// .then(assetRepo::save) .saveUsing(assetRepo::save)
// .then(HsHostingAssetEntityValidatorRegistry::validateInContext) .validateContext()
// // In this last step we need the entity and the mapped resource instance, .mapUsing(e -> mapper.map(e, HsHostingAssetResource.class))
// // which is exactly what a postmapper takes as arguments. .revampProperties();
// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties
final var saved = validated(assetRepo.save(current));
final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped); return ResponseEntity.ok(mapped);
} }
@ -155,6 +157,8 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
} }
}; };
final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER @SuppressWarnings("unchecked")
= HsHostingAssetEntityValidatorRegistry::postprocessProperties; final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource)
-> HsHostingAssetEntityValidatorRegistry.forType(entity.getType())
.revampProperties(entity, (Map<String, Object>) resource.getConfig());
} }

View File

@ -0,0 +1,63 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.Map;
import java.util.function.Function;
/**
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAssetEntity into a readable API.
*/
public class HsHostingAssetEntityProcessor {
private final HsEntityValidator<HsHostingAssetEntity> validator;
private HsHostingAssetEntity entity;
private HsHostingAssetResource resource;
public HsHostingAssetEntityProcessor(final HsHostingAssetEntity entity) {
this.entity = entity;
this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType());
}
/// validates the entity itself including its properties
public HsHostingAssetEntityProcessor validateEntity() {
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
return this;
}
/// hashing passwords etc.
@SuppressWarnings("unchecked")
public HsHostingAssetEntityProcessor prepareForSave() {
validator.prepareProperties(entity);
return this;
}
public HsHostingAssetEntityProcessor saveUsing(final Function<HsHostingAssetEntity, HsHostingAssetEntity> saveFunction) {
entity = saveFunction.apply(entity);
return this;
}
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
public HsHostingAssetEntityProcessor validateContext() {
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
return this;
}
/// maps entity to JSON resource representation
public HsHostingAssetEntityProcessor mapUsing(
final Function<HsHostingAssetEntity, HsHostingAssetResource> mapFunction) {
resource = mapFunction.apply(entity);
return this;
}
/// removes write-only-properties and ads computed-properties
@SuppressWarnings("unchecked")
public HsHostingAssetResource revampProperties() {
final var revampedProps = validator.revampProperties(entity, (Map<String, Object>) resource.getConfig());
resource.setConfig(revampedProps);
return resource;
}
}

View File

@ -45,10 +45,16 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
} }
@Override @Override
public List<String> validate(final HsHostingAssetEntity assetEntity) { public List<String> validateEntity(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate( return sequentiallyValidate(
() -> validateEntityReferencesAndProperties(assetEntity), () -> validateEntityReferencesAndProperties(assetEntity),
() -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem () -> validateIdentifierPattern(assetEntity)
);
}
@Override
public List<String> validateContext(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate(
() -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getBookingItem()),
() -> optionallyValidate(assetEntity.getParentAsset()), () -> optionallyValidate(assetEntity.getParentAsset()),
() -> validateAgainstSubEntities(assetEntity) () -> validateAgainstSubEntities(assetEntity)
@ -82,14 +88,14 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) { private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
return assetEntity != null return assetEntity != null
? enrich(prefix(assetEntity.toShortString(), "parentAsset"), ? enrich(prefix(assetEntity.toShortString(), "parentAsset"),
HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity)) HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity))
: emptyList(); : emptyList();
} }
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) { private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null return bookingItem != null
? enrich(prefix(bookingItem.toShortString(), "bookingItem"), ? enrich(prefix(bookingItem.toShortString(), "bookingItem"),
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
: emptyList(); : emptyList();
} }

View File

@ -4,7 +4,6 @@ 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.hosting.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import java.util.*; import java.util.*;
@ -40,22 +39,6 @@ public class HsHostingAssetEntityValidatorRegistry {
return validators.keySet(); return validators.keySet();
} }
public static List<String> doValidate(final HsHostingAssetEntity hostingAsset) {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(hostingAsset.getType());
return validator.validate(hostingAsset);
}
public static HsHostingAssetEntity validated(final HsHostingAssetEntity entityToSave) {
MultiValidationException.throwInvalid(doValidate(entityToSave));
return entityToSave;
}
public static void postprocessProperties(final HsHostingAssetEntity entity, final HsHostingAssetResource resource) {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType());
final var config = validator.postProcess(entity, asMap(resource));
resource.setConfig(config);
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static Map<String, Object> asMap(final HsHostingAssetResource resource) { private static Map<String, Object> asMap(final HsHostingAssetResource resource) {
if (resource.getConfig() instanceof Map map) { if (resource.getConfig() instanceof Map map) {

View File

@ -1,6 +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.hash.LinuxEtcShadowHashGenerator;
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;
@ -31,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).hashedUsing(HashProcessor.Algorithm.SHA512).writeOnly()); passwordProperty("password").minLength(8).maxLength(40).hashedUsing(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
} }
@Override @Override

View File

@ -96,7 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
validateDebitTransaction(requestBody, violations); validateDebitTransaction(requestBody, violations);
validateCreditTransaction(requestBody, violations); validateCreditTransaction(requestBody, violations);
validateAssetValue(requestBody, violations); validateAssetValue(requestBody, violations);
MultiValidationException.throwInvalid(violations); MultiValidationException.throwIfNotEmpty(violations);
} }
private static void validateDebitTransaction( private static void validateDebitTransaction(

View File

@ -98,7 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
validateSubscriptionTransaction(requestBody, violations); validateSubscriptionTransaction(requestBody, violations);
validateCancellationTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations);
validateshareCount(requestBody, violations); validateshareCount(requestBody, violations);
MultiValidationException.throwInvalid(violations); MultiValidationException.throwIfNotEmpty(violations);
} }
private static void validateSubscriptionTransaction( private static void validateSubscriptionTransaction(

View File

@ -32,7 +32,8 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
return String.join(".", parts); return String.join(".", parts);
} }
public abstract List<String> validate(final E entity); public abstract List<String> validateEntity(final E entity);
public abstract List<String> validateContext(final E entity);
public final List<Map<String, Object>> properties() { public final List<Map<String, Object>> properties() {
return Arrays.stream(propertyValidators) return Arrays.stream(propertyValidators)
@ -60,7 +61,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
} }
@SafeVarargs @SafeVarargs
protected static List<String> sequentiallyValidate(final Supplier<List<String>>... validators) { public static List<String> sequentiallyValidate(final Supplier<List<String>>... validators) {
return new ArrayList<>(stream(validators) return new ArrayList<>(stream(validators)
.map(Supplier::get) .map(Supplier::get)
.filter(violations -> !violations.isEmpty()) .filter(violations -> !violations.isEmpty())
@ -89,13 +90,20 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
} }
public Map<String, Object> postProcess(final E entity, final Map<String, Object> config) { public void prepareProperties(final E entity) {
stream(propertyValidators).forEach(p -> {
if ( p.isWriteOnly() && p.isComputed()) {
entity.directProps().put(p.propertyName, p.compute(entity));
}
});
}
public Map<String, Object> revampProperties(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()) { } else if (p.isReadOnly() && p.isComputed()) {
copy.put(p.propertyName, p.compute(entity)); copy.put(p.propertyName, p.compute(entity));
} }
}); });

View File

@ -1,13 +1,13 @@
package net.hostsharing.hsadminng.hs.validation; package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm; import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.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 java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry; import static net.hostsharing.hsadminng.mapper.Array.insertAfterEntry;
@Setter @Setter
@ -34,10 +34,9 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
public PasswordProperty hashedUsing(final Algorithm algorithm) { public PasswordProperty hashedUsing(final Algorithm algorithm) {
this.hashedUsing = algorithm; this.hashedUsing = algorithm;
// FIXME: computedBy is too late, we need preprocess
computedBy((entity) computedBy((entity)
-> ofNullable(entity.getDirectValue(propertyName, String.class)) -> ofNullable(entity.getDirectValue(propertyName, String.class))
.map(password -> hashAlgorithm(algorithm).withRandomSalt().generate(password)) .map(password -> hash(password).using(algorithm).withRandomSalt().generate())
.orElse(null)); .orElse(null));
return self(); return self();
} }

View File

@ -1,41 +0,0 @@
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");
}
}

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hash;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class LinuxEtcShadowHashGeneratorUnitTest {
final String GIVEN_PASSWORD = "given password";
final String WRONG_PASSWORD = "wrong password";
final String GIVEN_SALT = "0123456789abcdef";
// generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above)
final String GIVEN_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/";
final String GIVEN_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7";
@Test
void verifiesPasswordAgainstSha512HashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_SHA512_HASH); // throws exception if wrong
}
@Test
void verifiesPasswordAgainstYescryptHashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_YESCRYPT_HASH); // throws exception if wrong
}
@Test
void verifiesHashedPasswordWithRandomSalt() {
final var hash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate();
hash(GIVEN_PASSWORD).verify(hash); // throws exception if wrong
}
@Test
void verifiesHashedPasswordWithGivenSalt() {
final var givenPasswordHash =hash(GIVEN_PASSWORD).using(SHA512).withSalt(GIVEN_SALT).generate();
hash(GIVEN_PASSWORD).verify(givenPasswordHash); // throws exception if wrong
}
@Test
void throwsExceptionForInvalidPassword() {
final var givenPasswordHash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate();
final var throwable = catchThrowable(() ->
hash(WRONG_PASSWORD).verify(givenPasswordHash) // throws exception if wrong);
);
assertThat(throwable).hasMessage("invalid password");
}
}

View File

@ -16,6 +16,12 @@ public class TestHsBookingItem {
.project(TEST_PROJECT) .project(TEST_PROJECT)
.type(HsBookingItemType.CLOUD_SERVER) .type(HsBookingItemType.CLOUD_SERVER)
.caption("test cloud server booking item") .caption("test cloud server booking item")
.resources(Map.ofEntries(
entry("CPUs", 2),
entry("RAM", 4),
entry("SSD", 50),
entry("Traffic", 250)
))
.validity(Range.closedInfinite(LocalDate.of(2020, 1, 15))) .validity(Range.closedInfinite(LocalDate.of(2020, 1, 15)))
.build(); .build();

View File

@ -55,13 +55,13 @@ class HsCloudServerBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}", "{type=boolean, propertyName=active, defaultValue=true}",
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", "{type=integer, propertyName=CPUs, min=1, max=32, required=true}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}",
"{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}", "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true}",
"{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}");
} }
@Test @Test

View File

@ -63,17 +63,17 @@ class HsManagedServerBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", "{type=integer, propertyName=CPUs, min=1, max=32, required=true}",
"{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true}",
"{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=SSD, unit=GB, min=25, max=1000, step=25, required=true, isTotalsValidator=true, thresholdPercentage=200}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, defaultValue=0, isTotalsValidator=true, thresholdPercentage=200}",
"{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}", "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=true, thresholdPercentage=200}",
"{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}", "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], defaultValue=BASIC}",
"{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}", "{type=boolean, propertyName=SLA-EMail}", // TODO.impl: falseIf-validation is missing in output
"{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}", "{type=boolean, propertyName=SLA-Maria}",
"{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}", "{type=boolean, propertyName=SLA-PgSQL}",
"{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}", "{type=boolean, propertyName=SLA-Office}",
"{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}"); "{type=boolean, propertyName=SLA-Web}");
} }
@Test @Test

View File

@ -55,12 +55,12 @@ class HsManagedWebspaceBookingItemValidatorUnitTest {
// then // then
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
"{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true, isTotalsValidator=false}", "{type=integer, propertyName=SSD, unit=GB, min=1, max=100, step=1, required=true}",
"{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}", "{type=integer, propertyName=HDD, unit=GB, min=0, max=250, step=10}",
"{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}", "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true}",
"{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}", "{type=integer, propertyName=Multi, min=1, max=100, step=1, defaultValue=1}",
"{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}", "{type=integer, propertyName=Daemons, min=0, max=10, defaultValue=0}",
"{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}", "{type=boolean, propertyName=Online Office Server}",
"{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, isTotalsValidator=false}"); "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], defaultValue=BASIC}");
} }
} }

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
import io.restassured.RestAssured; import io.restassured.RestAssured;
import io.restassured.http.ContentType; import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.HsadminNgApplication; import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
@ -523,6 +524,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.identifier("fir01-temp") .identifier("fir01-temp")
.caption("some test-unix-user") .caption("some test-unix-user")
.build()); .build());
LinuxEtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
RestAssured // @formatter:off RestAssured // @formatter:off
.given() .given()
@ -575,7 +577,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user");
assertThat(asset.getConfig().toString()).isEqualTo(""" assertThat(asset.getConfig().toString()).isEqualTo("""
{ {
"password": "Ein Passwort mit 4 Zeichengruppen!", "password": "$6$Jr5w/Y8zo8pCkqg7$/rePRbvey3R6Sz/02YTlTQcRt5qdBPTj2h5.hz.rB8NfIoND8pFOjeB7orYcPs9JNf3JDxPP2V.6MQlE5BwAY/",
"shell": "/bin/bash", "shell": "/bin/bash",
"totpKey": "0x1234567890abcdef0123456789abcdef" "totpKey": "0x1234567890abcdef0123456789abcdef"
} }

View File

@ -29,7 +29,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
// when // when
final var result = validator.validate(cloudServerHostingAssetEntity); final var result = validator.validateEntity(cloudServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
@ -49,7 +49,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
// when // when
final var result = validator.validate(cloudServerHostingAssetEntity); final var result = validator.validateEntity(cloudServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
@ -76,7 +76,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when // when
final var result = validator.validate(mangedServerHostingAssetEntity); final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
@ -96,7 +96,7 @@ class HsCloudServerHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when // when
final var result = validator.validate(mangedServerHostingAssetEntity); final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(

View File

@ -1,16 +1,10 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators; package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
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 org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable; import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.api.Assertions.entry;
class HsHostingAssetEntityValidatorRegistryUnitTest { class HsHostingAssetEntityValidatorRegistryUnitTest {
@ -41,24 +35,4 @@ class HsHostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.UNIX_USER HsHostingAssetType.UNIX_USER
); );
} }
@Test
void validatedDoesNotThrowAnExceptionForValidEntity() {
final var givenBookingItem = HsBookingItemEntity.builder()
.type(HsBookingItemType.CLOUD_SERVER)
.resources(Map.ofEntries(
entry("CPUs", 4),
entry("RAM", 20),
entry("SSD", 50),
entry("Traffic", 250)
))
.build();
final var validEntity = HsHostingAssetEntity.builder()
.type(HsHostingAssetType.CLOUD_SERVER)
.bookingItem(givenBookingItem)
.identifier("vm1234")
.caption("some valid cloud server")
.build();
HsHostingAssetEntityValidatorRegistry.validated(validEntity);
}
} }

View File

@ -1,35 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class HsHostingAssetEntityValidatorUnitTest {
@Test
void validThrowsException() {
// given
final var managedServerHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER)
.identifier("vm1234")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build())
.parentAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build())
.assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_SERVER).build())
.build();
// when
final var result = catchThrowable( ()-> HsHostingAssetEntityValidatorRegistry.validated(managedServerHostingAssetEntity));
// then
assertThat(result.getMessage()).contains(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null"
);
}
}

View File

@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
import static java.util.Map.entry; import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_CLOUD_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -19,7 +21,7 @@ class HsManagedServerHostingAssetValidatorUnitTest {
final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder()
.type(MANAGED_SERVER) .type(MANAGED_SERVER)
.identifier("vm1234") .identifier("vm1234")
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.parentAsset(HsHostingAssetEntity.builder().build()) .parentAsset(HsHostingAssetEntity.builder().build())
.assignedToAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build())
.config(Map.ofEntries( .config(Map.ofEntries(
@ -31,12 +33,12 @@ class HsManagedServerHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType());
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-???????-?:null", "'MANAGED_SERVER:vm1234.parentAsset' must be null but is set to D-1234500:test project:test project booking item",
"'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-???????-?:null", "'MANAGED_SERVER:vm1234.assignedToAsset' must be null but is set to D-1234500:test project:test project booking item",
"'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2", "'MANAGED_SERVER:vm1234.config.monit_max_cpu_usage' is expected to be at least 10 but is 2",
"'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101", "'MANAGED_SERVER:vm1234.config.monit_max_ram_usage' is expected to be at most 100 but is 101",
"'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'"); "'MANAGED_SERVER:vm1234.config.monit_max_hdd_usage' is expected to be of type class java.lang.Integer, but is of type 'String'");
@ -53,7 +55,7 @@ class HsManagedServerHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when // when
final var result = validator.validate(mangedServerHostingAssetEntity); final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
@ -68,17 +70,17 @@ class HsManagedServerHostingAssetValidatorUnitTest {
.identifier("xyz00") .identifier("xyz00")
.parentAsset(HsHostingAssetEntity.builder().build()) .parentAsset(HsHostingAssetEntity.builder().build())
.assignedToAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build())
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM)
.build(); .build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when // when
final var result = validator.validate(mangedServerHostingAssetEntity); final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
"'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER", "'MANAGED_SERVER:xyz00.bookingItem' must be of type MANAGED_SERVER but is of type CLOUD_SERVER",
"'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-???????-?:null", "'MANAGED_SERVER:xyz00.parentAsset' must be null but is set to D-1234500:test project:test cloud server booking item",
"'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-???????-?:null"); "'MANAGED_SERVER:xyz00.assignedToAsset' must be null but is set to D-1234500:test project:test cloud server booking item");
} }
} }

View File

@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.Map;
import java.util.stream.Stream;
import static java.util.Map.entry; import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@ -70,7 +71,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
.build(); .build();
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = validator.validateContext(mangedWebspaceHostingAssetEntity);
// then // then
assertThat(result).isEmpty(); assertThat(result).isEmpty();
@ -88,7 +89,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
.build(); .build();
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity);
// then // then
assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'"); assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'");
@ -109,7 +110,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
.build(); .build();
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity);
// then // then
assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'"); assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'");
@ -131,7 +132,10 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
.build(); .build();
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = Stream.concat(
validator.validateEntity(mangedWebspaceHostingAssetEntity).stream(),
validator.validateContext(mangedWebspaceHostingAssetEntity).stream())
.toList();
// then // then
assertThat(result).isEmpty(); assertThat(result).isEmpty();
@ -154,7 +158,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest {
.build(); .build();
// when // when
final var result = validator.validate(mangedWebspaceHostingAssetEntity); final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity);
// then // then
assertThat(result).containsExactly( assertThat(result).containsExactly(

View File

@ -1,11 +1,14 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators; package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
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 org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Map; import java.util.HashMap;
import java.util.stream.Stream;
import static java.util.Map.ofEntries;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_SERVER_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.booking.item.TestHsBookingItem.TEST_MANAGED_WEBSPACE_BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@ -21,32 +24,55 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.caption("some managed server") .caption("some managed server")
.bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM)
.build(); .build();
private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() private final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder()
.type(MANAGED_WEBSPACE) .type(MANAGED_WEBSPACE)
.bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("abc00") .identifier("abc00")
.build();; .build();
private final HsHostingAssetEntity GIVEN_VALID_UNIX_USER_HOSTING_ASSET = HsHostingAssetEntity.builder()
.type(UNIX_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp")
.caption("some valid test UnixUser")
.config(new HashMap<>(ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("totpKey", "0x123456789abcdef01234"),
entry("password", "Hallo Computer, lass mich rein!")
)))
.build();
@Test
void preparesUnixUser() {
// given
final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET;
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
validator.prepareProperties(unixUserHostingAsset);
// then
assertThat(unixUserHostingAsset.getConfig()).containsExactlyInAnyOrderEntriesOf(ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("totpKey", "0x123456789abcdef01234"),
entry("password", "$6$Ly3LbsArtL5u4EVt$i/ayIEvm0y4bjkFB6wbg8imbRIaw4mAA4gqYRVyoSkj.iIxJKS3KiRkSjP8gweNcpKL0Q0N31EadT8fCnWErL.")
));
}
@Test @Test
void validatesValidUnixUser() { void validatesValidUnixUser() {
// given // given
final var unixUserHostingAsset = HsHostingAssetEntity.builder() final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET;
.type(UNIX_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp")
.caption("some valid test UnixUser")
.config(Map.ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("totpKey", "0x123456789abcdef01234"),
entry("password", "Hallo Computer, lass mich rein!")
))
.build();
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when // when
final var result = validator.validate(unixUserHostingAsset); final var result = Stream.concat(
validator.validateEntity(unixUserHostingAsset).stream(),
validator.validateContext(unixUserHostingAsset).stream()
).toList();
// then // then
assertThat(result).isEmpty(); assertThat(result).isEmpty();
@ -60,7 +86,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.identifier("abc00-temp") .identifier("abc00-temp")
.caption("some test UnixUser with invalid properties") .caption("some test UnixUser with invalid properties")
.config(Map.ofEntries( .config(ofEntries(
entry("SSD hard quota", 100), entry("SSD hard quota", 100),
entry("SSD soft quota", 200), entry("SSD soft quota", 200),
entry("HDD hard quota", 100), entry("HDD hard quota", 100),
@ -74,7 +100,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when // when
final var result = validator.validate(unixUserHostingAsset); final var result = validator.validateEntity(unixUserHostingAsset);
// then // then
assertThat(result).containsExactlyInAnyOrder( assertThat(result).containsExactlyInAnyOrder(
@ -101,13 +127,31 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when // when
final var result = validator.validate(unixUserHostingAsset); final var result = validator.validateEntity(unixUserHostingAsset);
// then // then
assertThat(result).containsExactly( assertThat(result).containsExactly(
"'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'"); "'identifier' expected to match '^abc00$|^abc00-[a-z0-9]+$', but is 'xyz99-temp'");
} }
@Test
void revampsUnixUser() {
// given
final var unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET;
final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
final var result = validator.revampProperties(unixUserHostingAsset, unixUserHostingAsset.getConfig());
// then
assertThat(result).containsExactlyInAnyOrderEntriesOf(ofEntries(
entry("SSD hard quota", 50),
entry("SSD soft quota", 40),
entry("homedir", "/home/pacs/abc00/users/temp")
));
}
@Test @Test
void describesItsProperties() { void describesItsProperties() {
// given // given
@ -125,7 +169,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, hashedUsing=SHA512, undisclosed=true}" "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=SHA512, undisclosed=true}"
); );
} }
} }

View File

@ -8,8 +8,8 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static net.hostsharing.hsadminng.hash.HashProcessor.Algorithm.SHA512; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.HashProcessor.hashAlgorithm; import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
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 net.hostsharing.hsadminng.mapper.PatchableMapWrapper.entry;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -115,6 +115,6 @@ class PasswordPropertyUnitTest {
}); });
// then // then
hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong
} }
} }