From 409f5e97c7c6c9413bc77a1bc195aaec85d40c65 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 1 Jul 2024 15:53:50 +0200 Subject: [PATCH] integrate-sha512-password-hashing (#68) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/68 Reviewed-by: Marc Sandlus --- build.gradle | 2 +- .../errors/MultiValidationException.java | 2 +- .../hsadminng/hash/HashProcessor.java | 107 ----------------- .../hash/LinuxEtcShadowHashGenerator.java | 112 ++++++++++++++++++ .../HsBookingItemEntityValidator.java | 15 +-- .../HsBookingItemEntityValidatorRegistry.java | 6 +- .../asset/HsHostingAssetController.java | 46 +++---- .../HsHostingAssetEntityProcessor.java | 63 ++++++++++ .../HsHostingAssetEntityValidator.java | 14 ++- ...HsHostingAssetEntityValidatorRegistry.java | 17 --- .../HsUnixUserHostingAssetValidator.java | 4 +- ...OfficeCoopAssetsTransactionController.java | 2 +- ...OfficeCoopSharesTransactionController.java | 2 +- .../hs/validation/HsEntityValidator.java | 20 +++- .../hs/validation/PasswordProperty.java | 7 +- .../hsadminng/hash/HashProcessorUnitTest.java | 41 ------- .../LinuxEtcShadowHashGeneratorUnitTest.java | 51 ++++++++ .../hs/booking/item/TestHsBookingItem.java | 6 + ...oudServerBookingItemValidatorUnitTest.java | 14 +-- ...gedServerBookingItemValidatorUnitTest.java | 18 +-- ...dWebspaceBookingItemValidatorUnitTest.java | 14 +-- ...sHostingAssetControllerAcceptanceTest.java | 4 +- ...udServerHostingAssetValidatorUnitTest.java | 8 +- ...gAssetEntityValidatorRegistryUnitTest.java | 26 ---- ...HsHostingAssetEntityValidatorUnitTest.java | 35 ------ ...edServerHostingAssetValidatorUnitTest.java | 20 ++-- ...WebspaceHostingAssetValidatorUnitTest.java | 14 ++- ...UnixUserHostingAssetValidatorUnitTest.java | 84 +++++++++---- .../validation/PasswordPropertyUnitTest.java | 6 +- 29 files changed, 419 insertions(+), 341 deletions(-) delete mode 100644 src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java diff --git a/build.gradle b/build.gradle index a4cc262f..63f4a996 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,7 @@ dependencies { implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' 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.iban4j:iban4j:3.2.7-RELEASE' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0' diff --git a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java index a6ba69e8..c8e721a2 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/MultiValidationException.java @@ -15,7 +15,7 @@ public class MultiValidationException extends ValidationException { ); } - public static void throwInvalid(final List violations) { + public static void throwIfNotEmpty(final List violations) { if (!violations.isEmpty()) { throw new MultiValidationException(violations); } diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java b/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java deleted file mode 100644 index d10ee565..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashProcessor.java +++ /dev/null @@ -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"); - } - } - } -} diff --git a/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java new file mode 100644 index 00000000..c030b830 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGenerator.java @@ -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 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); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java index 5cd0d71a..82a20e54 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidator.java @@ -20,22 +20,23 @@ public class HsBookingItemEntityValidator extends HsEntityValidator validate(final HsBookingItemEntity bookingItem) { + @Override + public List validateEntity(final HsBookingItemEntity bookingItem) { + return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); + } + + @Override + public List validateContext(final HsBookingItemEntity bookingItem) { return sequentiallyValidate( - () -> validateProperties(bookingItem), () -> optionallyValidate(bookingItem.getParentItem()), () -> validateAgainstSubEntities(bookingItem) ); } - private List validateProperties(final HsBookingItemEntity bookingItem) { - return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem)); - } - private static List optionallyValidate(final HsBookingItemEntity bookingItem) { return bookingItem != null ? enrich(prefix(bookingItem.toShortString(), ""), - HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) : emptyList(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java index e067781e..388855ff 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsBookingItemEntityValidatorRegistry.java @@ -45,11 +45,13 @@ public class HsBookingItemEntityValidatorRegistry { } public static List 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) { - MultiValidationException.throwInvalid(doValidate(entityToSave)); + MultiValidationException.throwIfNotEmpty(doValidate(entityToSave)); return entityToSave; } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index b0e5cd62..ca4c4a3e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; 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.generated.api.v1.api.HsHostingAssetsApi; @@ -21,11 +22,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; import jakarta.persistence.PersistenceContext; import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.function.BiConsumer; -import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidatorRegistry.validated; - @RestController 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 resources = mapper.mapList(entities, HsHostingAssetResource.class); + final var resources = mapper.mapList(entities, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(resources); } @@ -70,16 +70,21 @@ public class HsHostingAssetController implements HsHostingAssetsApi { 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 = MvcUriComponentsBuilder.fromController(getClass()) .path("/api/hs/hosting/assets/{id}") - .buildAndExpand(saved.getUuid()) + .buildAndExpand(mapped.getUuid()) .toUri(); - final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.created(uri).body(mapped); } @@ -123,21 +128,18 @@ public class HsHostingAssetController implements HsHostingAssetsApi { 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 -// validate(current) // self-validation, hashing passwords etc. -// .then(HsHostingAssetEntityValidatorRegistry::prepareForSave) // hashing passwords etc. -// .then(assetRepo::save) -// .then(HsHostingAssetEntityValidatorRegistry::validateInContext) -// // In this last step we need the entity and the mapped resource instance, -// // which is exactly what a postmapper takes as arguments. -// .then(this::mapToResource) using postProcessProperties to remove write-only + add read-only properties + final var mapped = new HsHostingAssetEntityProcessor(entity) + .validateEntity() + .prepareForSave() + .saveUsing(assetRepo::save) + .validateContext() + .mapUsing(e -> mapper.map(e, HsHostingAssetResource.class)) + .revampProperties(); - final var saved = validated(assetRepo.save(current)); - final var mapped = mapper.map(saved, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } @@ -155,6 +157,8 @@ public class HsHostingAssetController implements HsHostingAssetsApi { } }; - final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER - = HsHostingAssetEntityValidatorRegistry::postprocessProperties; + @SuppressWarnings("unchecked") + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) + -> HsHostingAssetEntityValidatorRegistry.forType(entity.getType()) + .revampProperties(entity, (Map) resource.getConfig()); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java new file mode 100644 index 00000000..5e270c86 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityProcessor.java @@ -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 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 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 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) resource.getConfig()); + resource.setConfig(revampedProps); + return resource; + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java index 9f4a6e61..8508ae1e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidator.java @@ -45,10 +45,16 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator validate(final HsHostingAssetEntity assetEntity) { + public List validateEntity(final HsHostingAssetEntity assetEntity) { return sequentiallyValidate( () -> validateEntityReferencesAndProperties(assetEntity), - () -> validateIdentifierPattern(assetEntity), // might need proper parentAsset or billingItem + () -> validateIdentifierPattern(assetEntity) + ); + } + + @Override + public List validateContext(final HsHostingAssetEntity assetEntity) { + return sequentiallyValidate( () -> optionallyValidate(assetEntity.getBookingItem()), () -> optionallyValidate(assetEntity.getParentAsset()), () -> validateAgainstSubEntities(assetEntity) @@ -82,14 +88,14 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator optionallyValidate(final HsHostingAssetEntity assetEntity) { return assetEntity != null ? enrich(prefix(assetEntity.toShortString(), "parentAsset"), - HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validate(assetEntity)) + HsHostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity)) : emptyList(); } private static List optionallyValidate(final HsBookingItemEntity bookingItem) { return bookingItem != null ? enrich(prefix(bookingItem.toShortString(), "bookingItem"), - HsBookingItemEntityValidatorRegistry.doValidate(bookingItem)) + HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem)) : emptyList(); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a5331f81..a6c30712 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -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.generated.api.v1.model.HsHostingAssetResource; import net.hostsharing.hsadminng.hs.validation.HsEntityValidator; -import net.hostsharing.hsadminng.errors.MultiValidationException; import java.util.*; @@ -40,22 +39,6 @@ public class HsHostingAssetEntityValidatorRegistry { return validators.keySet(); } - public static List 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") private static Map asMap(final HsHostingAssetResource resource) { if (resource.getConfig() instanceof Map map) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java index 1b7b01dc..309404f6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidator.java @@ -1,6 +1,6 @@ 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.HsHostingAssetType; import net.hostsharing.hsadminng.hs.validation.PropertiesProvider; @@ -31,7 +31,7 @@ class HsUnixUserHostingAssetValidator extends HsHostingAssetEntityValidator { .withDefault("/bin/false"), stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir), 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 diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java index 6279ad05..8ec1d956 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionController.java @@ -96,7 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse validateDebitTransaction(requestBody, violations); validateCreditTransaction(requestBody, violations); validateAssetValue(requestBody, violations); - MultiValidationException.throwInvalid(violations); + MultiValidationException.throwIfNotEmpty(violations); } private static void validateDebitTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java index f90d5276..78b41c9f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionController.java @@ -98,7 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar validateSubscriptionTransaction(requestBody, violations); validateCancellationTransaction(requestBody, violations); validateshareCount(requestBody, violations); - MultiValidationException.throwInvalid(violations); + MultiValidationException.throwIfNotEmpty(violations); } private static void validateSubscriptionTransaction( diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index bf755bd2..13cb3f05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -32,7 +32,8 @@ public abstract class HsEntityValidator { return String.join(".", parts); } - public abstract List validate(final E entity); + public abstract List validateEntity(final E entity); + public abstract List validateContext(final E entity); public final List> properties() { return Arrays.stream(propertyValidators) @@ -60,7 +61,7 @@ public abstract class HsEntityValidator { } @SafeVarargs - protected static List sequentiallyValidate(final Supplier>... validators) { + public static List sequentiallyValidate(final Supplier>... validators) { return new ArrayList<>(stream(validators) .map(Supplier::get) .filter(violations -> !violations.isEmpty()) @@ -89,13 +90,20 @@ public abstract class HsEntityValidator { throw new IllegalArgumentException("Integer value (or null) expected, but got " + value); } - public Map postProcess(final E entity, final Map 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 revampProperties(final E entity, final Map config) { final var copy = new HashMap<>(config); stream(propertyValidators).forEach(p -> { - // FIXME: maybe move to ValidatableProperty.postProcess(...)? - if ( p.isWriteOnly()) { + if (p.isWriteOnly()) { copy.remove(p.propertyName); - } else if (p.isComputed()) { + } else if (p.isReadOnly() && p.isComputed()) { copy.put(p.propertyName, p.compute(entity)); } }); diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java index 6f285595..37a8146f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/PasswordProperty.java @@ -1,13 +1,13 @@ package net.hostsharing.hsadminng.hs.validation; -import net.hostsharing.hsadminng.hash.HashProcessor.Algorithm; +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm; import lombok.Setter; import java.util.List; import java.util.stream.Stream; 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; @Setter @@ -34,10 +34,9 @@ public class PasswordProperty extends StringProperty { 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)) + .map(password -> hash(password).using(algorithm).withRandomSalt().generate()) .orElse(null)); return self(); } diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java deleted file mode 100644 index 6fc39578..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashProcessorUnitTest.java +++ /dev/null @@ -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"); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java new file mode 100644 index 00000000..c5abcc08 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hash/LinuxEtcShadowHashGeneratorUnitTest.java @@ -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"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java index b2b43df9..bcb2baac 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/TestHsBookingItem.java @@ -16,6 +16,12 @@ public class TestHsBookingItem { .project(TEST_PROJECT) .type(HsBookingItemType.CLOUD_SERVER) .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))) .build(); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java index 9258a4a1..b5307cd7 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsCloudServerBookingItemValidatorUnitTest.java @@ -55,13 +55,13 @@ class HsCloudServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=boolean, propertyName=active, required=false, defaultValue=true, isTotalsValidator=false}", - "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=SSD, unit=GB, min=0, max=1000, step=25, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=HDD, unit=GB, min=0, max=4000, step=250, required=false, defaultValue=0, isTotalsValidator=false}", - "{type=integer, propertyName=Traffic, unit=GB, min=250, max=10000, step=250, required=true, isTotalsValidator=false}", - "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, isTotalsValidator=false}"); + "{type=boolean, propertyName=active, defaultValue=true}", + "{type=integer, propertyName=CPUs, min=1, max=32, required=true}", + "{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}", + "{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}", + "{type=enumeration, propertyName=SLA-Infrastructure, values=[BASIC, EXT8H, EXT4H, EXT2H]}"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java index 1fe754ea..11020d92 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedServerBookingItemValidatorUnitTest.java @@ -63,17 +63,17 @@ class HsManagedServerBookingItemValidatorUnitTest { // then assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( - "{type=integer, propertyName=CPUs, min=1, max=32, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=RAM, unit=GB, min=1, max=128, 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}", "{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=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], required=false, defaultValue=BASIC, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-EMail, required=false, defaultValue=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Maria, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-PgSQL, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Office, required=false, isTotalsValidator=false}", - "{type=boolean, propertyName=SLA-Web, required=false, isTotalsValidator=false}"); + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT8H, EXT4H, EXT2H], defaultValue=BASIC}", + "{type=boolean, propertyName=SLA-EMail}", // TODO.impl: falseIf-validation is missing in output + "{type=boolean, propertyName=SLA-Maria}", + "{type=boolean, propertyName=SLA-PgSQL}", + "{type=boolean, propertyName=SLA-Office}", + "{type=boolean, propertyName=SLA-Web}"); } @Test diff --git a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java index dd9081ee..e75cd551 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/booking/item/validators/HsManagedWebspaceBookingItemValidatorUnitTest.java @@ -55,12 +55,12 @@ class HsManagedWebspaceBookingItemValidatorUnitTest { // then 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=HDD, unit=GB, min=0, max=250, step=10, required=false, isTotalsValidator=false}", - "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true, isTotalsValidator=false}", - "{type=integer, propertyName=Multi, min=1, max=100, step=1, required=false, defaultValue=1, isTotalsValidator=false}", - "{type=integer, propertyName=Daemons, min=0, max=10, required=false, defaultValue=0, isTotalsValidator=false}", - "{type=boolean, propertyName=Online Office Server, required=false, isTotalsValidator=false}", - "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], required=false, defaultValue=BASIC, 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}", + "{type=integer, propertyName=Traffic, unit=GB, min=10, max=1000, step=10, required=true}", + "{type=integer, propertyName=Multi, min=1, max=100, step=1, defaultValue=1}", + "{type=integer, propertyName=Daemons, min=0, max=10, defaultValue=0}", + "{type=boolean, propertyName=Online Office Server}", + "{type=enumeration, propertyName=SLA-Platform, values=[BASIC, EXT24H], defaultValue=BASIC}"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java index 11bfc45c..021fe02a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerAcceptanceTest.java @@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset; import io.restassured.RestAssured; import io.restassured.http.ContentType; 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.HsBookingItemRepository; import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType; @@ -523,6 +524,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup .identifier("fir01-temp") .caption("some test-unix-user") .build()); + LinuxEtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7"); RestAssured // @formatter:off .given() @@ -575,7 +577,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup assertThat(asset.getCaption()).isEqualTo("some patched test-unix-user"); assertThat(asset.getConfig().toString()).isEqualTo(""" { - "password": "Ein Passwort mit 4 Zeichengruppen!", + "password": "$6$Jr5w/Y8zo8pCkqg7$/rePRbvey3R6Sz/02YTlTQcRt5qdBPTj2h5.hz.rB8NfIoND8pFOjeB7orYcPs9JNf3JDxPP2V.6MQlE5BwAY/", "shell": "/bin/bash", "totpKey": "0x1234567890abcdef0123456789abcdef" } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java index fff0fd56..69fe01bb 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsCloudServerHostingAssetValidatorUnitTest.java @@ -29,7 +29,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // when - final var result = validator.validate(cloudServerHostingAssetEntity); + final var result = validator.validateEntity(cloudServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -49,7 +49,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { // when - final var result = validator.validate(cloudServerHostingAssetEntity); + final var result = validator.validateEntity(cloudServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -76,7 +76,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -96,7 +96,7 @@ class HsCloudServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java index 32c098f3..881b5c5f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistryUnitTest.java @@ -1,16 +1,10 @@ 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 org.junit.jupiter.api.Test; -import java.util.Map; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.assertj.core.api.Assertions.entry; class HsHostingAssetEntityValidatorRegistryUnitTest { @@ -41,24 +35,4 @@ class HsHostingAssetEntityValidatorRegistryUnitTest { 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); - } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java deleted file mode 100644 index 73776e89..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorUnitTest.java +++ /dev/null @@ -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" - ); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java index 2eb7f581..fd8d4800 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidatorUnitTest.java @@ -8,6 +8,8 @@ import org.junit.jupiter.api.Test; import java.util.Map; 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 org.assertj.core.api.Assertions.assertThat; @@ -19,7 +21,7 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var mangedWebspaceHostingAssetEntity = HsHostingAssetEntity.builder() .type(MANAGED_SERVER) .identifier("vm1234") - .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.MANAGED_SERVER).build()) + .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .parentAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build()) .config(Map.ofEntries( @@ -31,12 +33,12 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedWebspaceHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( - "'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", + "'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-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_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'"); @@ -53,7 +55,7 @@ class HsManagedServerHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( @@ -68,17 +70,17 @@ class HsManagedServerHostingAssetValidatorUnitTest { .identifier("xyz00") .parentAsset(HsHostingAssetEntity.builder().build()) .assignedToAsset(HsHostingAssetEntity.builder().build()) - .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .bookingItem(TEST_CLOUD_SERVER_BOOKING_ITEM) .build(); final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); // when - final var result = validator.validate(mangedServerHostingAssetEntity); + final var result = validator.validateEntity(mangedServerHostingAssetEntity); // then assertThat(result).containsExactlyInAnyOrder( "'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.assignedToAsset' 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-1234500:test project:test cloud server booking item"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java index 7b981b68..1d2c6d24 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidatorUnitTest.java @@ -7,6 +7,7 @@ import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; import org.junit.jupiter.api.Test; import java.util.Map; +import java.util.stream.Stream; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; @@ -70,7 +71,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateContext(mangedWebspaceHostingAssetEntity); // then assertThat(result).isEmpty(); @@ -88,7 +89,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly("'identifier' expected to match '^abc[0-9][0-9]$', but is 'xyz00'"); @@ -109,7 +110,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly("'MANAGED_WEBSPACE:abc00.config.unknown' is not expected but is set to 'some value'"); @@ -131,7 +132,10 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = Stream.concat( + validator.validateEntity(mangedWebspaceHostingAssetEntity).stream(), + validator.validateContext(mangedWebspaceHostingAssetEntity).stream()) + .toList(); // then assertThat(result).isEmpty(); @@ -154,7 +158,7 @@ class HsManagedWebspaceHostingAssetValidatorUnitTest { .build(); // when - final var result = validator.validate(mangedWebspaceHostingAssetEntity); + final var result = validator.validateEntity(mangedWebspaceHostingAssetEntity); // then assertThat(result).containsExactly( diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java index 2c92d69b..5ef61da9 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsUnixUserHostingAssetValidatorUnitTest.java @@ -1,11 +1,14 @@ 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.HsHostingAssetType; 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_WEBSPACE_BOOKING_ITEM; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; @@ -21,32 +24,55 @@ class HsUnixUserHostingAssetValidatorUnitTest { .caption("some managed server") .bookingItem(TEST_MANAGED_SERVER_BOOKING_ITEM) .build(); - private HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() + private final HsHostingAssetEntity TEST_MANAGED_WEBSPACE_HOSTING_ASSET = HsHostingAssetEntity.builder() .type(MANAGED_WEBSPACE) .bookingItem(TEST_MANAGED_WEBSPACE_BOOKING_ITEM) .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) .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 void validatesValidUnixUser() { // given - final var unixUserHostingAsset = HsHostingAssetEntity.builder() - .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 unixUserHostingAsset = GIVEN_VALID_UNIX_USER_HOSTING_ASSET; final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = Stream.concat( + validator.validateEntity(unixUserHostingAsset).stream(), + validator.validateContext(unixUserHostingAsset).stream() + ).toList(); // then assertThat(result).isEmpty(); @@ -60,7 +86,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) .identifier("abc00-temp") .caption("some test UnixUser with invalid properties") - .config(Map.ofEntries( + .config(ofEntries( entry("SSD hard quota", 100), entry("SSD soft quota", 200), entry("HDD hard quota", 100), @@ -74,7 +100,7 @@ class HsUnixUserHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = validator.validateEntity(unixUserHostingAsset); // then assertThat(result).containsExactlyInAnyOrder( @@ -101,13 +127,31 @@ class HsUnixUserHostingAssetValidatorUnitTest { final var validator = HsHostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType()); // when - final var result = validator.validate(unixUserHostingAsset); + final var result = validator.validateEntity(unixUserHostingAsset); // then assertThat(result).containsExactly( "'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 void describesItsProperties() { // 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=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=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}" ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java index b694c304..2350b288 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/validation/PasswordPropertyUnitTest.java @@ -8,8 +8,8 @@ import java.util.ArrayList; 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.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512; +import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash; 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; @@ -115,6 +115,6 @@ class PasswordPropertyUnitTest { }); // then - hashAlgorithm(SHA512).withHash(result).verify("some password"); // throws exception if wrong + hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong } }