integrate-sha512-password-hashing (#68)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/68 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@ -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()) {
|
||||
throw new MultiValidationException(violations);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -20,22 +20,23 @@ public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingIte
|
||||
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(
|
||||
() -> validateProperties(bookingItem),
|
||||
() -> optionallyValidate(bookingItem.getParentItem()),
|
||||
() -> 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) {
|
||||
return bookingItem != null
|
||||
? enrich(prefix(bookingItem.toShortString(), ""),
|
||||
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
|
||||
HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
||||
: emptyList();
|
||||
}
|
||||
|
||||
|
@ -45,11 +45,13 @@ public class HsBookingItemEntityValidatorRegistry {
|
||||
}
|
||||
|
||||
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) {
|
||||
MultiValidationException.throwInvalid(doValidate(entityToSave));
|
||||
MultiValidationException.throwIfNotEmpty(doValidate(entityToSave));
|
||||
return entityToSave;
|
||||
}
|
||||
}
|
||||
|
@ -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<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER
|
||||
= HsHostingAssetEntityValidatorRegistry::postprocessProperties;
|
||||
@SuppressWarnings("unchecked")
|
||||
final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource)
|
||||
-> HsHostingAssetEntityValidatorRegistry.forType(entity.getType())
|
||||
.revampProperties(entity, (Map<String, Object>) resource.getConfig());
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -45,10 +45,16 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> validate(final HsHostingAssetEntity assetEntity) {
|
||||
public List<String> validateEntity(final HsHostingAssetEntity assetEntity) {
|
||||
return sequentiallyValidate(
|
||||
() -> 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.getParentAsset()),
|
||||
() -> validateAgainstSubEntities(assetEntity)
|
||||
@ -82,14 +88,14 @@ public abstract class HsHostingAssetEntityValidator extends HsEntityValidator<Hs
|
||||
private static List<String> 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<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
|
||||
return bookingItem != null
|
||||
? enrich(prefix(bookingItem.toShortString(), "bookingItem"),
|
||||
HsBookingItemEntityValidatorRegistry.doValidate(bookingItem))
|
||||
HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
||||
: emptyList();
|
||||
}
|
||||
|
||||
|
@ -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<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")
|
||||
private static Map<String, Object> asMap(final HsHostingAssetResource resource) {
|
||||
if (resource.getConfig() instanceof Map map) {
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -32,7 +32,8 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||
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() {
|
||||
return Arrays.stream(propertyValidators)
|
||||
@ -60,7 +61,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
||||
}
|
||||
|
||||
@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)
|
||||
.map(Supplier::get)
|
||||
.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);
|
||||
}
|
||||
|
||||
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);
|
||||
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));
|
||||
}
|
||||
});
|
||||
|
@ -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<PasswordProperty> {
|
||||
|
||||
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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user