add-mariadb-instance-database-and-user-validations (#75)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #75
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-07-12 10:54:47 +02:00
parent 9d2692add3
commit 46fce275ae
27 changed files with 813 additions and 226 deletions

View File

@ -1,5 +1,6 @@
## HostingAsset Type Structure
### Domain
```plantuml
@ -11,8 +12,6 @@ package Booking #feb28c {
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
@ -43,20 +42,21 @@ BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_CLOUD_SERVER *==> BI_CLOUD_SERVER
HA_MANAGED_SERVER *==> BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP
HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_DNS_SETUP o..> HA_MANAGED_WEBSPACE
HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER
HA_DOMAIN_SMTP_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_SMTP_SETUP o..> HA_MANAGED_WEBSPACE
HA_DOMAIN_MBOX_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_MBOX_SETUP o..> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ADDRESS *==> HA_DOMAIN_MBOX_SETUP
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER
@ -70,6 +70,7 @@ package Legend #white {
}
Booking -down[hidden]->Legend
```
### MariaDB
```plantuml
@ -81,8 +82,6 @@ package Booking #feb28c {
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
@ -110,16 +109,16 @@ BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_CLOUD_SERVER *==> BI_CLOUD_SERVER
HA_MANAGED_SERVER *==> BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_MARIADB_INSTANCE *==> HA_MANAGED_SERVER
HA_MARIADB_USER *==> HA_MARIADB_INSTANCE
HA_MARIADB_USER o..> HA_MANAGED_WEBSPACE
HA_MARIADB_DATABASE *==> HA_MANAGED_WEBSPACE
HA_MARIADB_USER *==> HA_MANAGED_WEBSPACE
HA_MARIADB_USER o..> HA_MARIADB_INSTANCE
HA_MARIADB_DATABASE *==> HA_MARIADB_USER
HA_MARIADB_DATABASE o..> HA_MARIADB_INSTANCE
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER
@ -133,6 +132,7 @@ package Legend #white {
}
Booking -down[hidden]->Legend
```
### PostgreSQL
```plantuml
@ -144,8 +144,6 @@ package Booking #feb28c {
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
@ -173,9 +171,9 @@ BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_CLOUD_SERVER *==> BI_CLOUD_SERVER
HA_MANAGED_SERVER *==> BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE *==> BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
@ -196,4 +194,5 @@ package Legend #white {
}
Booking -down[hidden]->Legend
```
This code generated was by HsHostingAssetType.main, do not amend manually.

View File

@ -0,0 +1,89 @@
package net.hostsharing.hsadminng.hash;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.function.BiFunction;
import java.util.random.RandomGenerator;
import lombok.Getter;
/**
* Usage-example to generate hash:
* HashGenerator.using(LINUX_SHA512).withRandomSalt().hash("plaintext password");
*
* Usage-example to verify hash:
* HashGenerator.fromHash("hashed password).verify("plaintext password");
*/
@Getter
public final class HashGenerator {
private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int RANDOM_SALT_LENGTH = 16;
private static final String RANDOM_SALT_CHARACTERS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
public enum Algorithm {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"),
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*");
final BiFunction<HashGenerator, String, String> implementation;
final String prefix;
Algorithm(BiFunction<HashGenerator, String, String> implementation, final String prefix) {
this.implementation = implementation;
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 final Algorithm algorithm;
private String salt;
public static HashGenerator using(final Algorithm algorithm) {
return new HashGenerator(algorithm);
}
private HashGenerator(final Algorithm algorithm) {
this.algorithm = algorithm;
}
public String hash(final String plaintextPassword) {
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
}
return algorithm.implementation.apply(this, plaintextPassword);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public HashGenerator withSalt(final String salt) {
this.salt = salt;
return this;
}
public HashGenerator withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
final var stringBuilder = new StringBuilder(RANDOM_SALT_LENGTH);
for (int i = 0; i < RANDOM_SALT_LENGTH; ++i) {
int randomIndex = random.nextInt(RANDOM_SALT_CHARACTERS.length());
stringBuilder.append(RANDOM_SALT_CHARACTERS.charAt(randomIndex));
}
return withSalt(stringBuilder.toString());
}
}

View File

@ -1,107 +1,31 @@
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) {
public static String hash(final HashGenerator generator, final String payload) {
if (generator.getSalt() == null) {
throw new IllegalStateException("no salt given");
}
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
return NativeCryptLibrary.INSTANCE.crypt(payload, "$" + generator.getAlgorithm().prefix + "$" + generator.getSalt());
}
return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt);
public static void verify(final String givenHash, final String payload) {
final var parts = givenHash.split("\\$");
if (parts.length < 3 || parts.length > 5) {
throw new IllegalArgumentException("hash with unknown hash method: " + givenHash);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
final var algorithm = HashGenerator.Algorithm.byPrefix(parts[1]);
final var salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
final var calcualatedHash = HashGenerator.using(algorithm).withSalt(salt).hash(payload);
if (!calcualatedHash.equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
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 {

View File

@ -0,0 +1,35 @@
package net.hostsharing.hsadminng.hash;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MySQLNativePasswordHashGenerator {
public static String hash(final HashGenerator generator, final String password) {
// TODO.impl: if a random salt is generated or not should be part of the algorithm definition
// if (generator.getSalt() != null) {
// throw new IllegalStateException("salt not supported");
// }
try {
final var sha1 = MessageDigest.getInstance("SHA-1");
final var firstHash = sha1.digest(password.getBytes());
final var secondHash = sha1.digest(firstHash);
return "*" + bytesToHex(secondHash).toUpperCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-1 algorithm not found", e);
}
}
private static String bytesToHex(byte[] bytes) {
final var hexString = new StringBuilder();
for (byte b : bytes) {
final var hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
}
}

View File

@ -100,13 +100,13 @@ public enum HsHostingAssetType implements Node {
MARIADB_USER( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MARIADB_INSTANCE),
assignedTo(MANAGED_WEBSPACE)),
requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
MARIADB_DATABASE( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or MARIADB_USER?
assignedTo(MARIADB_INSTANCE)), // TODO.spec: or swapping parent+assignedTo?
requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner
assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied
IP_NUMBER(
inGroup("Server"),

View File

@ -26,6 +26,9 @@ public class HostingAssetEntityValidatorRegistry {
register(DOMAIN_SMTP_SETUP, new HsDomainSmtpSetupHostingAssetValidator());
register(DOMAIN_MBOX_SETUP, new HsDomainMboxSetupHostingAssetValidator());
register(EMAIL_ADDRESS, new HsEMailAddressHostingAssetValidator());
register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator());
register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator());
register(MARIADB_DATABASE, new HsMariaDbDatabaseHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -0,0 +1,25 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsMariaDbDatabaseHostingAssetValidator extends HostingAssetEntityValidator {
public HsMariaDbDatabaseHostingAssetValidator() {
super(
MARIADB_DATABASE,
AlarmContact.isOptional(),
stringProperty("encoding").matchesRegEx("[a-z0-9_]+").maxLength(24).provided("latin1", "utf8").withDefault("utf8"));
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
}
}

View File

@ -0,0 +1,37 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_INSTANCE;
class HsMariaDbInstanceHostingAssetValidator extends HostingAssetEntityValidator {
final static String DEFAULT_INSTANCE_IDENTIFIER_SUFFIX = "|MariaDB.default"; // TODO.spec: specify instance naming
public HsMariaDbInstanceHostingAssetValidator() {
super(
MARIADB_INSTANCE,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile(
"^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier()
+ DEFAULT_INSTANCE_IDENTIFIER_SUFFIX)
+ "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(
pa.getIdentifier() + DEFAULT_INSTANCE_IDENTIFIER_SUFFIX));
}
}
}

View File

@ -0,0 +1,33 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
class HsMariaDbUserHostingAssetValidator extends HostingAssetEntityValidator {
public HsMariaDbUserHostingAssetValidator() {
super(
MARIADB_USER,
AlarmContact.isOptional(),
// TODO.impl: we need to be able to suppress updating of fields etc., something like this:
// withFieldValidation(
// referenceProperty(alarmContact).isOptional(),
// referenceProperty(parentAsset).isWriteOnce(),
// referenceProperty(assignedToAsset).isWriteOnce(),
// );
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashGenerator.Algorithm.MYSQL_NATIVE).writeOnly());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$");
}
}

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
@ -30,7 +30,8 @@ class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
.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(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(HashGenerator.Algorithm.LINUX_SHA512).writeOnly());
// TODO.spec: public SSH keys?
}
@Override

View File

@ -1,13 +1,13 @@
package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hash.HashGenerator.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.LinuxEtcShadowHashGenerator.hash;
import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry;
@Setter
@ -36,7 +36,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
this.hashedUsing = algorithm;
computedBy((entity)
-> ofNullable(entity.getDirectValue(propertyName, String.class))
.map(password -> hash(password).using(algorithm).withRandomSalt().generate())
.map(password -> HashGenerator.using(algorithm).withRandomSalt().hash(password))
.orElse(null));
return self();
}

View File

@ -19,6 +19,7 @@ components:
- EMAIL_ADDRESS
- PGSQL_USER
- PGSQL_DATABASE
- MARIADB_INSTANCE
- MARIADB_USER
- MARIADB_DATABASE

View File

@ -18,6 +18,7 @@ create type HsHostingAssetType as enum (
'EMAIL_ADDRESS',
'PGSQL_USER',
'PGSQL_DATABASE',
'MARIADB_INSTANCE',
'MARIADB_USER',
'MARIADB_DATABASE'
);
@ -74,8 +75,9 @@ begin
when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP'
when 'PGSQL_USER' then 'MANAGED_WEBSPACE'
when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE'
when 'MARIADB_USER' then 'MANAGED_WEBSPACE'
when 'MARIADB_DATABASE' then 'MANAGED_WEBSPACE'
when 'MARIADB_INSTANCE' then 'MANAGED_SERVER'
when 'MARIADB_USER' then 'MARIADB_INSTANCE'
when 'MARIADB_DATABASE' then 'MARIADB_INSTANCE'
else raiseException(format('[400] unknown asset type %s', NEW.type::text))
end);

View File

@ -25,6 +25,8 @@ declare
webUnixUserUuid uuid;
domainSetupUuid uuid;
domainMBoxSetupUuid uuid;
mariaDbInstanceUuid uuid;
mariaDbUserUuid uuid;
begin
currentTask := 'creating hosting-asset test-data ' || givenProjectCaption;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
@ -69,14 +71,20 @@ begin
select uuid_generate_v4() into webUnixUserUuid;
select uuid_generate_v4() into domainSetupUuid;
select uuid_generate_v4() into domainMBoxSetupUuid;
select uuid_generate_v4() into mariaDbInstanceUuid;
select uuid_generate_v4() into mariaDbUserUuid;
debitorNumberSuffix := relatedDebitor.debitorNumberSuffix;
defaultPrefix := relatedDebitor.defaultPrefix;
insert into hs_hosting_asset
(uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config)
values (managedServerUuid, managedServerBI.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb),
values
(managedServerUuid, managedServerBI.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb),
(uuid_generate_v4(), cloudServerBI.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb),
(managedWebspaceUuid, managedWebspaceBI.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb),
(mariaDbInstanceUuid, null, 'MARIADB_INSTANCE', managedServerUuid, null, 'vm10' || debitorNumberSuffix || '.MariaDB.default', 'some default MariaDB instance','{}'::jsonb),
(mariaDbUserUuid, null, 'MARIADB_USER', mariaDbInstanceUuid, managedWebspaceUuid, defaultPrefix || '01_web', 'some default MariaDB user', '{ "password": "<TODO:replace-by-encrypted-mariadb-password"}'::jsonb ),
(uuid_generate_v4(), null, 'MARIADB_DATABASE', mariaDbInstanceUuid, mariaDbUserUuid, defaultPrefix || '01_web', 'some default MariaDB database','{ "encryption": "utf8", "collation": "utf8"}'::jsonb ),
(uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb),
(webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb),
(domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb),

View File

@ -0,0 +1,56 @@
package net.hostsharing.hsadminng.hash;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512;
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.MYSQL_NATIVE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class HashGeneratorUnitTest {
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_LINUX_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/";
final String GIVEN_LINUX_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7";
@Test
void verifiesLinuxPasswordAgainstSha512HashFromMkpasswd() {
LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_SHA512_HASH, GIVEN_PASSWORD); // throws exception if wrong
}
@Test
void verifiesLinuxPasswordAgainstYescryptHashFromMkpasswd() {
LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_YESCRYPT_HASH, GIVEN_PASSWORD); // throws exception if wrong
}
@Test
void verifiesHashedLinuxPasswordWithRandomSalt() {
final var hash = HashGenerator.using(LINUX_SHA512).withRandomSalt().hash(GIVEN_PASSWORD);
LinuxEtcShadowHashGenerator.verify(hash, GIVEN_PASSWORD); // throws exception if wrong
}
@Test
void verifiesLinuxHashedPasswordWithGivenSalt() {
final var hash = HashGenerator.using(LINUX_SHA512).withSalt(GIVEN_SALT).hash(GIVEN_PASSWORD);
LinuxEtcShadowHashGenerator.verify(hash, GIVEN_PASSWORD); // throws exception if wrong
}
@Test
void throwsExceptionForInvalidLinuxPassword() {
final var hash = HashGenerator.using(LINUX_SHA512).withRandomSalt().hash(GIVEN_PASSWORD);
final var throwable = catchThrowable(() ->
LinuxEtcShadowHashGenerator.verify(hash, WRONG_PASSWORD)
);
assertThat(throwable).hasMessage("invalid password");
}
@Test
void verifiesMySqlNativePassword() {
final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234");
assertThat(hash).isEqualTo("*14F1A8C42F8B6D4662BB3ED290FD37BF135FE45C");
}
}

View File

@ -1,51 +0,0 @@
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

@ -3,7 +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.hash.HashGenerator;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
@ -537,7 +537,7 @@ class HsHostingAssetControllerAcceptanceTest extends ContextBasedTestWithCleanup
.identifier("fir01-temp")
.caption("some test-unix-user")
.build());
LinuxEtcShadowHashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
HashGenerator.nextSalt("Jr5w/Y8zo8pCkqg7");
RestAssured // @formatter:off
.given()

View File

@ -30,6 +30,7 @@ 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.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.office.contact.TestHsOfficeContact.TEST_CONTACT;
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
@ -363,6 +364,68 @@ public class HsHostingAssetControllerRestTest {
}
}
]
"""),
MARIADB_INSTANCE(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MARIADB_INSTANCE)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("vm1234|MariaDB.default")
.caption("some fake MariaDB instance")
.build()),
"""
[
{
"type": "MARIADB_INSTANCE",
"identifier": "vm1234|MariaDB.default",
"caption": "some fake MariaDB instance",
"alarmContact": null,
"config": {}
}
]
"""),
MARIADB_USER(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MARIADB_USER)
.identifier("xyz00_temp")
.caption("some fake MariaDB user")
.build()),
"""
[
{
"type": "MARIADB_USER",
"identifier": "xyz00_temp",
"caption": "some fake MariaDB user",
"alarmContact": null,
"config": {}
}
]
"""),
MARIADB_DATABASE(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.MARIADB_DATABASE)
.identifier("xyz00_temp")
.caption("some fake MariaDB database")
.config(Map.ofEntries(
entry("encoding", "latin1"),
entry("collation", "latin2")
))
.build()),
"""
[
{
"type": "MARIADB_DATABASE",
"identifier": "xyz00_temp",
"caption": "some fake MariaDB database",
"alarmContact": null,
"config": {
"encoding": "latin1",
"collation": "latin2"
}
}
]
""");
final HsHostingAssetType assetType;

View File

@ -41,7 +41,10 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"DOMAIN_HTTP_SETUP",
"DOMAIN_SMTP_SETUP",
"DOMAIN_MBOX_SETUP",
"EMAIL_ADDRESS"
"EMAIL_ADDRESS",
"MARIADB_INSTANCE",
"MARIADB_USER",
"MARIADB_DATABASE"
]
"""));
// @formatter:on

View File

@ -245,7 +245,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
// then
exactlyTheseAssetsAreReturned(
result,
"HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)");
"HsHostingAssetEntity(MANAGED_WEBSPACE, sec01, some Webspace, MANAGED_SERVER:vm1012, D-1000212:D-1000212 default project:separate ManagedWebspace)",
"HsHostingAssetEntity(MARIADB_INSTANCE, vm1012.MariaDB.default, some default MariaDB instance, MANAGED_SERVER:vm1012)");
}
}

View File

@ -129,9 +129,9 @@ class HsHostingAssetTypeUnitTest {
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_MARIADB_INSTANCE *==> HA_MANAGED_SERVER
HA_MARIADB_USER *==> HA_MARIADB_INSTANCE
HA_MARIADB_USER o..> HA_MANAGED_WEBSPACE
HA_MARIADB_DATABASE *==> HA_MANAGED_WEBSPACE
HA_MARIADB_USER *==> HA_MANAGED_WEBSPACE
HA_MARIADB_USER o..> HA_MARIADB_INSTANCE
HA_MARIADB_DATABASE *==> HA_MARIADB_USER
HA_MARIADB_DATABASE o..> HA_MARIADB_INSTANCE
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER

View File

@ -39,7 +39,10 @@ class HostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.DOMAIN_HTTP_SETUP,
HsHostingAssetType.DOMAIN_SMTP_SETUP,
HsHostingAssetType.DOMAIN_MBOX_SETUP,
HsHostingAssetType.EMAIL_ADDRESS
HsHostingAssetType.EMAIL_ADDRESS,
HsHostingAssetType.MARIADB_INSTANCE,
HsHostingAssetType.MARIADB_USER,
HsHostingAssetType.MARIADB_DATABASE
);
}
}

View File

@ -0,0 +1,117 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.stream.Stream;
import static java.util.Map.ofEntries;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_INSTANCE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET;
import static net.hostsharing.hsadminng.mapper.PatchMap.entry;
import static org.assertj.core.api.Assertions.assertThat;
class HsMariaDbDatabaseHostingAssetValidatorUnitTest {
private static final HsHostingAssetEntity GIVEN_MARIADB_INSTANCE = HsHostingAssetEntity.builder()
.type(MARIADB_INSTANCE)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("vm1234|MariaDB.default")
.caption("some valid test MariaDB-Instance")
.build();
private static final HsHostingAssetEntity GIVEN_MARIADB_USER = HsHostingAssetEntity.builder()
.type(MARIADB_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.assignedToAsset(GIVEN_MARIADB_INSTANCE)
.identifier("xyz00_temp")
.caption("some valid test MariaDB-User")
.config(new HashMap<>(ofEntries(
entry("password", "Hallo Datenbank, lass mich rein!")
)))
.build();
private static HsHostingAssetEntityBuilder givenValidMariaDbDatabaseBuilder() {
return HsHostingAssetEntity.builder()
.type(MARIADB_DATABASE)
.parentAsset(GIVEN_MARIADB_USER)
.assignedToAsset(GIVEN_MARIADB_INSTANCE)
.identifier("xyz00_temp")
.caption("some valid test MariaDB-Database")
.config(new HashMap<>(ofEntries(
entry("encoding", "latin1")
)));
}
@Test
void describesItsProperties() {
// given
final var validator = HostingAssetEntityValidatorRegistry.forType(givenValidMariaDbDatabaseBuilder().build().getType());
// when
final var props = validator.properties();
// then
assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder(
"{type=string, propertyName=encoding, matchesRegEx=[[a-z0-9_]+], maxLength=24, provided=[latin1, utf8], defaultValue=utf8}"
);
}
@Test
void validatesValidEntity() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbDatabaseBuilder().build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = Stream.concat(
validator.validateEntity(givenMariaDbUserHostingAsset).stream(),
validator.validateContext(givenMariaDbUserHostingAsset).stream()
).toList();
// then
assertThat(result).isEmpty();
}
@Test
void rejectsInvalidProperties() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbDatabaseBuilder()
.config(ofEntries(
entry("unknown", "wrong"),
entry("encoding", 10)
))
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = validator.validateEntity(givenMariaDbUserHostingAsset);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MARIADB_DATABASE:xyz00_temp.config.unknown' is not expected but is set to 'wrong'",
"'MARIADB_DATABASE:xyz00_temp.config.encoding' is expected to be of type String, but is of type Integer"
);
}
@Test
void rejectsInvalidIdentifier() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbDatabaseBuilder()
.identifier("xyz99-temp")
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = validator.validateEntity(givenMariaDbUserHostingAsset);
// then
assertThat(result).containsExactly(
"'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'");
}
}

View File

@ -0,0 +1,116 @@
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.HsHostingAssetEntity.HsHostingAssetEntityBuilder;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static java.util.Map.entry;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_INSTANCE;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsMariaDbInstanceHostingAssetValidator.DEFAULT_INSTANCE_IDENTIFIER_SUFFIX;
import static org.assertj.core.api.Assertions.assertThat;
class HsMariaDbInstanceHostingAssetValidatorUnitTest {
static HsHostingAssetEntityBuilder validEntityBuilder() {
return HsHostingAssetEntity.builder()
.type(MARIADB_INSTANCE)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier(TEST_MANAGED_SERVER_HOSTING_ASSET.getIdentifier() + DEFAULT_INSTANCE_IDENTIFIER_SUFFIX);
}
@Test
void containsExpectedProperties() {
// when
final var validator = HostingAssetEntityValidatorRegistry.forType(DOMAIN_SMTP_SETUP);
// then
assertThat(validator.properties()).map(Map::toString).isEmpty();
}
@Test
void preprocessesTakesIdentifierFromParent() {
// given
final var givenEntity = validEntityBuilder().build();
assertThat(givenEntity.getParentAsset().getIdentifier()).as("precondition failed").isEqualTo("vm1234");
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
// when
validator.preprocessEntity(givenEntity);
// then
assertThat(givenEntity.getIdentifier()).isEqualTo("vm1234|MariaDB.default");
}
@Test
void acceptsValidEntity() {
// given
final var givenEntity = validEntityBuilder().build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
// when
final var result = validator.validateEntity(givenEntity);
// then
assertThat(result).isEmpty();
}
@Test
void rejectsInvalidIdentifier() {
// given
final var givenEntity = validEntityBuilder().identifier("example.org").build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenEntity.getType());
// when
final var result = validator.validateEntity(givenEntity);
// then
assertThat(result).containsExactly(
"'identifier' expected to match '^\\Qvm1234|MariaDB.default\\E$', but is 'example.org'"
);
}
@Test
void rejectsInvalidReferencedEntities() {
// given
final var mangedServerHostingAssetEntity = validEntityBuilder()
.bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build())
.parentAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build())
.assignedToAsset(HsHostingAssetEntity.builder().type(MANAGED_WEBSPACE).build())
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MARIADB_INSTANCE:vm1234|MariaDB.default.bookingItem' must be null but is of type CLOUD_SERVER",
"'MARIADB_INSTANCE:vm1234|MariaDB.default.parentAsset' must be of type MANAGED_SERVER but is of type MANAGED_WEBSPACE",
"'MARIADB_INSTANCE:vm1234|MariaDB.default.assignedToAsset' must be null but is of type MANAGED_WEBSPACE");
}
@Test
void rejectsInvalidProperties() {
// given
final var mangedServerHostingAssetEntity = validEntityBuilder()
.config(Map.ofEntries(
entry("any", "false")
))
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType());
// when
final var result = validator.validateEntity(mangedServerHostingAssetEntity);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MARIADB_INSTANCE:vm1234|MariaDB.default.config.any' is not expected but is set to 'false'");
}
}

View File

@ -0,0 +1,122 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity.HsHostingAssetEntityBuilder;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.stream.Stream;
import static java.util.Map.ofEntries;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_INSTANCE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_SERVER_HOSTING_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.TestHsHostingAssetEntities.TEST_MANAGED_WEBSPACE_HOSTING_ASSET;
import static net.hostsharing.hsadminng.mapper.PatchMap.entry;
import static org.assertj.core.api.Assertions.assertThat;
class HsMariaDbUserHostingAssetValidatorUnitTest {
private static final HsHostingAssetEntity GIVEN_MARIADB_INSTANCE = HsHostingAssetEntity.builder()
.type(MARIADB_INSTANCE)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("vm1234|MariaDB.default")
.caption("some valid test MariaDB-Instance")
.build();
private static HsHostingAssetEntityBuilder givenValidMariaDbUserBuilder() {
return HsHostingAssetEntity.builder()
.type(MARIADB_USER)
.parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET)
.assignedToAsset(GIVEN_MARIADB_INSTANCE)
.identifier("xyz00_temp")
.caption("some valid test MariaDB-User")
.config(new HashMap<>(ofEntries(
entry("password", "Test1234")
)));
}
@Test
void describesItsProperties() {
// given
final var validator = HostingAssetEntityValidatorRegistry.forType(givenValidMariaDbUserBuilder().build().getType());
// when
final var props = validator.properties();
// then
assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder(
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=MYSQL_NATIVE, undisclosed=true}"
);
}
@Test
void preparesEntity() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbUserBuilder().build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
// HashGenerator.nextSalt("Ly3LbsArtL5u4EVt"); // not needed for mysql_native_password
validator.prepareProperties(givenMariaDbUserHostingAsset);
// then
assertThat(givenMariaDbUserHostingAsset.getConfig()).containsExactlyInAnyOrderEntriesOf(ofEntries(
entry("password", "*14F1A8C42F8B6D4662BB3ED290FD37BF135FE45C")
));
}
@Test
void validatesValidEntity() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbUserBuilder().build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = Stream.concat(
validator.validateEntity(givenMariaDbUserHostingAsset).stream(),
validator.validateContext(givenMariaDbUserHostingAsset).stream()
).toList();
// then
assertThat(result).isEmpty();
}
@Test
void rejectsInvalidProperties() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbUserBuilder()
.config(ofEntries(
entry("unknown", 100),
entry("password", "short")
))
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = validator.validateEntity(givenMariaDbUserHostingAsset);
// then
assertThat(result).containsExactlyInAnyOrder(
"'MARIADB_USER:xyz00_temp.config.unknown' is not expected but is set to '100'",
"'MARIADB_USER:xyz00_temp.config.password' length is expected to be at min 8 but length of provided value is 5",
"'MARIADB_USER:xyz00_temp.config.password' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters"
);
}
@Test
void rejectsInvalidIdentifier() {
// given
final var givenMariaDbUserHostingAsset = givenValidMariaDbUserBuilder()
.identifier("xyz99-temp")
.build();
final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType());
// when
final var result = validator.validateEntity(givenMariaDbUserHostingAsset);
// then
assertThat(result).containsExactly(
"'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'");
}
}

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import net.hostsharing.hsadminng.hash.HashGenerator;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import org.junit.jupiter.api.Test;
@ -50,7 +50,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var validator = HostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
HashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
validator.prepareProperties(unixUserHostingAsset);
// then
@ -141,7 +141,7 @@ class HsUnixUserHostingAssetValidatorUnitTest {
final var validator = HostingAssetEntityValidatorRegistry.forType(unixUserHostingAsset.getType());
// when
LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
HashGenerator.nextSalt("Ly3LbsArtL5u4EVt");
final var result = validator.revampProperties(unixUserHostingAsset, unixUserHostingAsset.getConfig());
// then
@ -169,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, computed=true, hashedUsing=SHA512, undisclosed=true}"
"{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=LINUX_SHA512, undisclosed=true}"
);
}
}

View File

@ -1,5 +1,6 @@
package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@ -8,8 +9,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512;
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;
@ -17,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class PasswordPropertyUnitTest {
private final ValidatableProperty<PasswordProperty, String> passwordProp =
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(SHA512).writeOnly();
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(LINUX_SHA512).writeOnly();
private final List<String> violations = new ArrayList<>();
@ParameterizedTest
@ -115,6 +115,6 @@ class PasswordPropertyUnitTest {
});
// then
hash("some password").using(SHA512).withRandomSalt().generate(); // throws exception if wrong
LinuxEtcShadowHashGenerator.verify(result, "some password"); // throws exception if wrong
}
}