From 29994a851a14e5bdff2354e89f0d4b2d57402905 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 12 Jul 2024 17:32:20 +0200 Subject: [PATCH 1/4] add PostgreSQLScramSHA256 password encoding (working, but needs cleanup) --- .../hsadminng/hash/HashGenerator.java | 3 +- .../hsadminng/hash/PostgreSQLScramSHA256.java | 61 +++++++++++++++++++ .../hsadminng/hash/HashGeneratorUnitTest.java | 34 ++++++++++- 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index 345f0ed0..c5a2cd3c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -31,7 +31,8 @@ public final class HashGenerator { public enum Algorithm { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"), - MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"); + MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"), + SCRAM_SHA256(PostgreSQLScramSHA256::hash, "*"); final BiFunction implementation; final String prefix; diff --git a/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java b/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java new file mode 100644 index 00000000..500909f1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hash/PostgreSQLScramSHA256.java @@ -0,0 +1,61 @@ +package net.hostsharing.hsadminng.hash; + +import lombok.SneakyThrows; + +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +public class PostgreSQLScramSHA256 { + + private static final String PBKDF_2_WITH_HMAC_SHA256 = "PBKDF2WithHmacSHA256"; + private static final String HMAC_SHA256 = "HmacSHA256"; + private static final String SHA256 = "SHA-256"; + private static final int ITERATIONS = 4096; + public static final int KEY_LENGTH_IN_BITS = 256; + + private static final PostgreSQLScramSHA256 scram = new PostgreSQLScramSHA256(); + + @SneakyThrows + public static String hash(final HashGenerator generator, final String password) { + if (generator.getSalt() == null) { + throw new IllegalStateException("no salt given"); + } + + final byte[] salt = generator.getSalt().getBytes(Charset.forName("latin1")); // Base64.getEncoder().encode(generator.getSalt().getBytes()); + final byte[] saltedPassword = scram.generateSaltedPassword(password, salt); + final byte[] clientKey = scram.hmacSHA256(saltedPassword, "Client Key".getBytes()); + final byte[] storedKey = MessageDigest.getInstance(SHA256).digest(clientKey); + final byte[] serverKey = scram.hmacSHA256(saltedPassword, "Server Key".getBytes()); + + return "SCRAM-SHA-256${iterations}:{base64EncodedSalt}${base64EncodedStoredKey}:{base64EncodedServerKey}" + .replace("{iterations}", Integer.toString(ITERATIONS)) + .replace("{base64EncodedSalt}", base64(salt)) + .replace("{base64EncodedStoredKey}", base64(storedKey)) + .replace("{base64EncodedServerKey}", base64(serverKey)); + } + + private static String base64(final byte[] salt) { + return Base64.getEncoder().encodeToString(salt); + } + + private byte[] generateSaltedPassword(String password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException { + final var spec = new PBEKeySpec(password.toCharArray(), salt, ITERATIONS, KEY_LENGTH_IN_BITS); + return SecretKeyFactory.getInstance(PBKDF_2_WITH_HMAC_SHA256).generateSecret(spec).getEncoded(); + } + + private byte[] hmacSHA256(byte[] key, byte[] message) + throws NoSuchAlgorithmException, InvalidKeyException { + final var mac = Mac.getInstance(HMAC_SHA256); + mac.init(new SecretKeySpec(key, HMAC_SHA256)); + return mac.doFinal(message); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java index 6c70bc8e..13207eb6 100644 --- a/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hash/HashGeneratorUnitTest.java @@ -2,8 +2,12 @@ package net.hostsharing.hsadminng.hash; import org.junit.jupiter.api.Test; +import java.nio.charset.Charset; +import java.util.Base64; + import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.LINUX_SHA512; import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.MYSQL_NATIVE; +import static net.hostsharing.hsadminng.hash.HashGenerator.Algorithm.SCRAM_SHA256; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -49,8 +53,36 @@ class HashGeneratorUnitTest { } @Test - void verifiesMySqlNativePassword() { + void generatesMySqlNativePasswordHash() { final var hash = HashGenerator.using(MYSQL_NATIVE).hash("Test1234"); assertThat(hash).isEqualTo("*14F1A8C42F8B6D4662BB3ED290FD37BF135FE45C"); } + + @Test + void generatePostgreSqlScramPasswordHash() { + //final var salt = new String(Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ==")); + final var postgresBase64Salt = Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ=="); + final var reEncodedSalt = Base64.getEncoder().encodeToString(postgresBase64Salt); + final var hash = HashGenerator.using(SCRAM_SHA256).withSalt(new String(postgresBase64Salt, Charset.forName("latin1"))).hash("Test1234"); + + assertThat(hash).isEqualTo( + "SCRAM-SHA-256$4096:3gsrkV5e1VZweIFiKfvoeQ==$/8I29AMTJ+7W9ceeKhc5LsfTrTHF6/+m/qstv2h0kpo=:+MDwFXgAjHHSnlqgU5adOPtW0qpbFUMrYp59Xs7ns0U="); + } + + // ALTER USER your_username WITH PASSWORD 'SCRAM-SHA-256$iterations:base64-encoded-salt$base64-encoded-stored-key$base64-encoded-server-key'; + + @Test + public void xxx() { + String postgresqlBase64Salt = "3gsrkV5e1VZweIFiKfvoeQ=="; // Example Base64 encoded salt from PostgreSQL + + // Decode the base64 salt using the standard Base64 decoder + byte[] decodedSalt = Base64.getDecoder().decode(postgresqlBase64Salt); + + // Re-encode the salt using the standard Base64 encoder + String javaBase64Salt = Base64.getEncoder().encodeToString(decodedSalt); + + // Print both the original and re-encoded salts + System.out.println("Orig PostgreSQL Base64 Salt: " + postgresqlBase64Salt); + System.out.println("Re-encoded Java Base64 Salt: " + javaBase64Salt); + } } -- 2.39.5 From 797db51aec46a17a70b858a9c62756e5863d7416 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Sat, 13 Jul 2024 06:36:46 +0200 Subject: [PATCH 2/4] add postgresql instance, database and user validators --- .../hsadminng/hash/HashGenerator.java | 2 +- .../hs/hosting/asset/HsHostingAssetType.java | 14 +- .../HostingAssetEntityValidator.java | 3 +- .../HostingAssetEntityValidatorRegistry.java | 3 + .../HsManagedServerHostingAssetValidator.java | 2 +- ...sManagedWebspaceHostingAssetValidator.java | 2 +- ...sMariaDbInstanceHostingAssetValidator.java | 2 +- ...stgreSqlDatabaseHostingAssetValidator.java | 26 ++++ ...greSqlDbInstanceHostingAssetValidator.java | 37 ++++++ ...HsPostgreSqlUserHostingAssetValidator.java | 33 +++++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 1 + .../7010-hs-hosting-asset.sql | 13 +- .../7018-hs-hosting-asset-test-data.sql | 11 +- .../hsadminng/hash/HashGeneratorUnitTest.java | 41 +++--- .../HsHostingAssetControllerRestTest.java | 62 +++++++++ ...ingAssetPropsControllerAcceptanceTest.java | 5 +- ...HostingAssetRepositoryIntegrationTest.java | 3 +- .../asset/HsHostingAssetTypeUnitTest.java | 6 +- ...gAssetEntityValidatorRegistryUnitTest.java | 5 +- ...stgreSqlHostingAssetValidatorUnitTest.java | 122 ++++++++++++++++++ ...DatabaseHostingAssetValidatorUnitTest.java | 117 +++++++++++++++++ ...InstanceHostingAssetValidatorUnitTest.java | 116 +++++++++++++++++ 22 files changed, 577 insertions(+), 49 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlInstanceHostingAssetValidatorUnitTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java index c5a2cd3c..5bc09cc6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/hash/HashGenerator.java @@ -32,7 +32,7 @@ public final class HashGenerator { LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"), MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"), - SCRAM_SHA256(PostgreSQLScramSHA256::hash, "*"); + SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256"); final BiFunction implementation; final String prefix; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index bf9563f8..d3f26644 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -82,17 +82,17 @@ public enum HsHostingAssetType implements Node { PGSQL_INSTANCE( // TODO.spec: identifier to be specified inGroup("PostgreSQL"), - requiredParent(MANAGED_SERVER)), + requiredParent(MANAGED_SERVER)), // TODO.spec: or MANAGED_WEBSPACE? PGSQL_USER( // named e.g. xyz00_abc inGroup("PostgreSQL"), - requiredParent(PGSQL_INSTANCE), - assignedTo(MANAGED_WEBSPACE)), + requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner + assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied PGSQL_DATABASE( // named e.g. xyz00_abc inGroup("PostgreSQL"), - requiredParent(MANAGED_WEBSPACE), // TODO.spec: or PGSQL_USER? - assignedTo(PGSQL_INSTANCE)), // TODO.spec: or swapping parent+assignedTo? + requiredParent(PGSQL_USER), // thus, the PGSQL_USER_USER:Agent becomes RBAC owner + assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied MARIADB_INSTANCE( // TODO.spec: identifier to be specified inGroup("MariaDB"), @@ -101,12 +101,12 @@ public enum HsHostingAssetType implements Node { MARIADB_USER( // named e.g. xyz00_abc inGroup("MariaDB"), requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner - assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied + assignedTo(MARIADB_INSTANCE)), MARIADB_DATABASE( // named e.g. xyz00_abc inGroup("MariaDB"), requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner - assignedTo(MARIADB_INSTANCE)), // keep in mind: no RBAC grants implied + assignedTo(MARIADB_INSTANCE)), IP_NUMBER( inGroup("Server"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java index 0c0282e0..7257f70c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidator.java @@ -32,7 +32,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator... properties) { super(properties); this.bookingItemReferenceValidation = new ReferenceValidator<>( @@ -213,6 +213,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java index 0397b79e..732c0285 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedServerHostingAssetValidator.java @@ -14,7 +14,7 @@ class HsManagedServerHostingAssetValidator extends HostingAssetEntityValidator { public HsManagedServerHostingAssetValidator() { super( MANAGED_SERVER, - AlarmContact.isOptional(), // hostmaster alert address is implicitly added + AlarmContact.isOptional(), // monitoring integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java index 8345c5fc..45e9e520 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsManagedWebspaceHostingAssetValidator.java @@ -10,7 +10,7 @@ class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator public HsManagedWebspaceHostingAssetValidator() { super( MANAGED_WEBSPACE, - AlarmContact.isOptional(), // hostmaster alert address is implicitly added + AlarmContact.isOptional(), NO_EXTRA_PROPERTIES); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbInstanceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbInstanceHostingAssetValidator.java index 88bfb50d..74acd9e6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbInstanceHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbInstanceHostingAssetValidator.java @@ -14,7 +14,7 @@ class HsMariaDbInstanceHostingAssetValidator extends HostingAssetEntityValidator public HsMariaDbInstanceHostingAssetValidator() { super( MARIADB_INSTANCE, - AlarmContact.isOptional(), // hostmaster alert address is implicitly added + AlarmContact.isOptional(), NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java new file mode 100644 index 00000000..a5cb9726 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java @@ -0,0 +1,26 @@ +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.PGSQL_DATABASE; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsPostgreSqlDatabaseHostingAssetValidator extends HostingAssetEntityValidator { + + public HsPostgreSqlDatabaseHostingAssetValidator() { + super( + PGSQL_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]+$"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java new file mode 100644 index 00000000..73d41758 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java @@ -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.PGSQL_DATABASE; + +class HsPostgreSqlDbInstanceHostingAssetValidator extends HostingAssetEntityValidator { + + final static String DEFAULT_INSTANCE_IDENTIFIER_SUFFIX = "|PgSql.default"; // TODO.spec: specify instance naming + + public HsPostgreSqlDbInstanceHostingAssetValidator() { + super( + PGSQL_DATABASE, + AlarmContact.isOptional(), + 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)); + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java new file mode 100644 index 00000000..385b41ab --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java @@ -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.PGSQL_USER; +import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty; + +class HsPostgreSqlUserHostingAssetValidator extends HostingAssetEntityValidator { + + public HsPostgreSqlUserHostingAssetValidator() { + super( + PGSQL_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.SCRAM_SHA256).writeOnly()); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + } +} diff --git a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml index d467031f..b531fe8a 100644 --- a/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml +++ b/src/main/resources/api-definition/hs-hosting/hs-hosting-asset-schemas.yaml @@ -17,6 +17,7 @@ components: - DOMAIN_MBOX_SETUP - EMAIL_ALIAS - EMAIL_ADDRESS + - PGSQL_INSTANCE - PGSQL_USER - PGSQL_DATABASE - MARIADB_INSTANCE diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql index ece84d9c..4497b675 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql @@ -16,6 +16,7 @@ create type HsHostingAssetType as enum ( 'DOMAIN_MBOX_SETUP', 'EMAIL_ALIAS', 'EMAIL_ADDRESS', + 'PGSQL_INSTANCE', 'PGSQL_USER', 'PGSQL_DATABASE', 'MARIADB_INSTANCE', @@ -48,6 +49,9 @@ create table if not exists hs_hosting_asset --changeset hosting-asset-TYPE-HIERARCHY-CHECK:1 endDelimiter:--// -- ---------------------------------------------------------------------------- +-- TODO.impl: this could be generated from HsHostingAssetType +-- also including a check for assignedToAssetUuud + create or replace function hs_hosting_asset_type_hierarchy_check_tf() returns trigger language plpgsql as $$ @@ -73,11 +77,14 @@ begin when 'DOMAIN_SMTP_SETUP' then 'DOMAIN_SETUP' when 'DOMAIN_MBOX_SETUP' then 'DOMAIN_SETUP' when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP' + + when 'PGSQL_INSTANCE' then 'MANAGED_SERVER' when 'PGSQL_USER' then 'MANAGED_WEBSPACE' - when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE' + when 'PGSQL_DATABASE' then 'PGSQL_USER' + when 'MARIADB_INSTANCE' then 'MANAGED_SERVER' - when 'MARIADB_USER' then 'MARIADB_INSTANCE' - when 'MARIADB_DATABASE' then 'MARIADB_INSTANCE' + when 'MARIADB_USER' then 'MANAGED_WEBSPACE' + when 'MARIADB_DATABASE' then 'MARIADB_USER' else raiseException(format('[400] unknown asset type %s', NEW.type::text)) end); diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql index c7fc4450..9e8f3317 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7018-hs-hosting-asset-test-data.sql @@ -27,6 +27,8 @@ declare domainMBoxSetupUuid uuid; mariaDbInstanceUuid uuid; mariaDbUserUuid uuid; + pgSqlInstanceUuid uuid; + PgSqlUserUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -73,6 +75,8 @@ begin select uuid_generate_v4() into domainMBoxSetupUuid; select uuid_generate_v4() into mariaDbInstanceUuid; select uuid_generate_v4() into mariaDbUserUuid; + select uuid_generate_v4() into pgSqlInstanceUuid; + select uuid_generate_v4() into pgSqlUserUuid; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; defaultPrefix := relatedDebitor.defaultPrefix; @@ -83,8 +87,11 @@ begin (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": " HA_MANAGED_WEBSPACE HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER - HA_PGSQL_USER *==> HA_PGSQL_INSTANCE - HA_PGSQL_USER o..> HA_MANAGED_WEBSPACE - HA_PGSQL_DATABASE *==> HA_MANAGED_WEBSPACE + HA_PGSQL_USER *==> HA_MANAGED_WEBSPACE + HA_PGSQL_USER o..> HA_PGSQL_INSTANCE + HA_PGSQL_DATABASE *==> HA_PGSQL_USER HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE HA_IP_NUMBER o..> HA_CLOUD_SERVER HA_IP_NUMBER o..> HA_MANAGED_SERVER diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java index 61bf0ea8..4b752663 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistryUnitTest.java @@ -42,7 +42,10 @@ class HostingAssetEntityValidatorRegistryUnitTest { HsHostingAssetType.EMAIL_ADDRESS, HsHostingAssetType.MARIADB_INSTANCE, HsHostingAssetType.MARIADB_USER, - HsHostingAssetType.MARIADB_DATABASE + HsHostingAssetType.MARIADB_DATABASE, + HsHostingAssetType.PGSQL_INSTANCE, + HsHostingAssetType.PGSQL_USER, + HsHostingAssetType.PGSQL_DATABASE ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..0886bb26 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java @@ -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 HsMariaPostgreSqlHostingAssetValidatorUnitTest { + + 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'"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..5d917f81 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java @@ -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.PGSQL_DATABASE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_INSTANCE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_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 HsPostgreSqlDatabaseHostingAssetValidatorUnitTest { + + private static final HsHostingAssetEntity GIVEN_PGSQL_INSTANCE = HsHostingAssetEntity.builder() + .type(PGSQL_INSTANCE) + .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) + .identifier("vm1234|PgSql.default") + .caption("some valid test PgSql-Instance") + .build(); + + private static final HsHostingAssetEntity GIVEN_PGSQL_USER = HsHostingAssetEntity.builder() + .type(PGSQL_USER) + .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) + .assignedToAsset(GIVEN_PGSQL_INSTANCE) + .identifier("xyz00_temp") + .caption("some valid test PgSql-User") + .config(new HashMap<>(ofEntries( + entry("password", "Hallo Datenbank, lass mich rein!") + ))) + .build(); + + private static HsHostingAssetEntityBuilder givenValidPgSqlDatabaseBuilder() { + return HsHostingAssetEntity.builder() + .type(PGSQL_DATABASE) + .parentAsset(GIVEN_PGSQL_USER) + .assignedToAsset(GIVEN_PGSQL_INSTANCE) + .identifier("xyz00_temp") + .caption("some valid test PgSql-Database") + .config(new HashMap<>(ofEntries( + entry("encoding", "LATIN1") + ))); + } + + @Test + void describesItsProperties() { + // given + final var validator = HostingAssetEntityValidatorRegistry.forType(givenValidPgSqlDatabaseBuilder().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 givenPgSqlUserHostingAsset = givenValidPgSqlDatabaseBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenPgSqlUserHostingAsset.getType()); + + // when + final var result = Stream.concat( + validator.validateEntity(givenPgSqlUserHostingAsset).stream(), + validator.validateContext(givenPgSqlUserHostingAsset).stream() + ).toList(); + + // then + assertThat(result).isEmpty(); + } + + @Test + void rejectsInvalidProperties() { + // given + final var givenPgSqlUserHostingAsset = givenValidPgSqlDatabaseBuilder() + .config(ofEntries( + entry("unknown", "wrong"), + entry("encoding", 10) + )) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenPgSqlUserHostingAsset.getType()); + + // when + final var result = validator.validateEntity(givenPgSqlUserHostingAsset); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'PGSQL_DATABASE:xyz00_temp.config.unknown' is not expected but is set to 'wrong'", + "'PGSQL_DATABASE:xyz00_temp.config.encoding' is expected to be of type String, but is of type Integer" + ); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var givenPgSqlUserHostingAsset = givenValidPgSqlDatabaseBuilder() + .identifier("xyz99-temp") + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenPgSqlUserHostingAsset.getType()); + + // when + final var result = validator.validateEntity(givenPgSqlUserHostingAsset); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlInstanceHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlInstanceHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..231bb773 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlInstanceHostingAssetValidatorUnitTest.java @@ -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 HsPostgreSqlInstanceHostingAssetValidatorUnitTest { + + 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'"); + } +} -- 2.39.5 From 2ad2418ccc8e464e584481f82200fd036ab71d29 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 15 Jul 2024 06:11:03 +0200 Subject: [PATCH 3/4] add TODO.spec for extensions --- .../validators/HsPostgreSqlDatabaseHostingAssetValidator.java | 2 ++ .../validators/HsPostgreSqlDbInstanceHostingAssetValidator.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java index a5cb9726..727d6e03 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java @@ -15,6 +15,8 @@ class HsPostgreSqlDatabaseHostingAssetValidator extends HostingAssetEntityValida AlarmContact.isOptional(), stringProperty("encoding").matchesRegEx("[A-Z0-9_]+").maxLength(24).provided("LATIN1", "UTF8").withDefault("UTF8") + + // TODO.spec: PostgreSQL extensions in instance and here? also decide which. Free selection or booleans/checkboxes? ); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java index 73d41758..ecdd5441 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDbInstanceHostingAssetValidator.java @@ -15,6 +15,8 @@ class HsPostgreSqlDbInstanceHostingAssetValidator extends HostingAssetEntityVali super( PGSQL_DATABASE, AlarmContact.isOptional(), + + // TODO.spec: PostgreSQL extensions in database and here? also decide which. Free selection or booleans/checkboxes? NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions } -- 2.39.5 From 07f0b00510765f9a76b9a6e0779b60d9ab85c485 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Mon, 15 Jul 2024 11:36:21 +0200 Subject: [PATCH 4/4] fixes after code-review --- .../hs/hosting/asset/HsHostingAssetType.java | 6 +-- ...sMariaDbDatabaseHostingAssetValidator.java | 2 +- .../HsMariaDbUserHostingAssetValidator.java | 2 +- ...stgreSqlDatabaseHostingAssetValidator.java | 2 +- ...HsPostgreSqlUserHostingAssetValidator.java | 2 +- .../asset/HsHostingAssetTypeUnitTest.java | 2 - ...DatabaseHostingAssetValidatorUnitTest.java | 3 +- ...iaDbUserHostingAssetValidatorUnitTest.java | 2 +- ...DatabaseHostingAssetValidatorUnitTest.java | 25 ++++++++++++- ...SqlUserHostingAssetValidatorUnitTest.java} | 37 ++++++++++--------- 10 files changed, 51 insertions(+), 32 deletions(-) rename src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/{HsMariaPostgreSqlHostingAssetValidatorUnitTest.java => HsPostgreSqlUserHostingAssetValidatorUnitTest.java} (73%) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java index d3f26644..6d9fa75e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetType.java @@ -91,8 +91,7 @@ public enum HsHostingAssetType implements Node { PGSQL_DATABASE( // named e.g. xyz00_abc inGroup("PostgreSQL"), - requiredParent(PGSQL_USER), // thus, the PGSQL_USER_USER:Agent becomes RBAC owner - assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied + requiredParent(PGSQL_USER)), // thus, the PGSQL_USER_USER:Agent becomes RBAC owner MARIADB_INSTANCE( // TODO.spec: identifier to be specified inGroup("MariaDB"), @@ -105,8 +104,7 @@ public enum HsHostingAssetType implements Node { MARIADB_DATABASE( // named e.g. xyz00_abc inGroup("MariaDB"), - requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner - assignedTo(MARIADB_INSTANCE)), + requiredParent(MARIADB_USER)), // thus, the MARIADB_USER:Agent becomes RBAC owner IP_NUMBER( inGroup("Server"), diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidator.java index c183ffbc..197dc9b6 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidator.java @@ -20,6 +20,6 @@ class HsMariaDbDatabaseHostingAssetValidator extends HostingAssetEntityValidator @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier(); - return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidator.java index 9ee6af9a..8e749e44 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidator.java @@ -28,6 +28,6 @@ class HsMariaDbUserHostingAssetValidator extends HostingAssetEntityValidator { @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); - return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java index 727d6e03..86e9900e 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidator.java @@ -23,6 +23,6 @@ class HsPostgreSqlDatabaseHostingAssetValidator extends HostingAssetEntityValida @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { final var webspaceIdentifier = assetEntity.getParentAsset().getParentAsset().getIdentifier(); - return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java index 385b41ab..8c91427d 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidator.java @@ -28,6 +28,6 @@ class HsPostgreSqlUserHostingAssetValidator extends HostingAssetEntityValidator @Override protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier(); - return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9_]+$"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java index 164e5cd2..09d9537a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetTypeUnitTest.java @@ -132,7 +132,6 @@ class HsHostingAssetTypeUnitTest { 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 HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE @@ -194,7 +193,6 @@ class HsHostingAssetTypeUnitTest { HA_PGSQL_USER *==> HA_MANAGED_WEBSPACE HA_PGSQL_USER o..> HA_PGSQL_INSTANCE HA_PGSQL_DATABASE *==> HA_PGSQL_USER - HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE HA_IP_NUMBER o..> HA_CLOUD_SERVER HA_IP_NUMBER o..> HA_MANAGED_SERVER HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidatorUnitTest.java index c5459b31..37c8fb85 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbDatabaseHostingAssetValidatorUnitTest.java @@ -40,7 +40,6 @@ class HsMariaDbDatabaseHostingAssetValidatorUnitTest { 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( @@ -112,6 +111,6 @@ class HsMariaDbDatabaseHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'"); + "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9_]+$', but is 'xyz99-temp'"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidatorUnitTest.java index 7ef55796..d5f4948e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidatorUnitTest.java @@ -117,6 +117,6 @@ class HsMariaDbUserHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'"); + "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9_]+$', but is 'xyz99-temp'"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java index 5d917f81..092c253b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlDatabaseHostingAssetValidatorUnitTest.java @@ -1,5 +1,7 @@ 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; @@ -40,7 +42,6 @@ class HsPostgreSqlDatabaseHostingAssetValidatorUnitTest { return HsHostingAssetEntity.builder() .type(PGSQL_DATABASE) .parentAsset(GIVEN_PGSQL_USER) - .assignedToAsset(GIVEN_PGSQL_INSTANCE) .identifier("xyz00_temp") .caption("some valid test PgSql-Database") .config(new HashMap<>(ofEntries( @@ -78,6 +79,26 @@ class HsPostgreSqlDatabaseHostingAssetValidatorUnitTest { assertThat(result).isEmpty(); } + @Test + void rejectsInvalidReferences() { + // given + final var givenPgSqlUserHostingAsset = givenValidPgSqlDatabaseBuilder() + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .parentAsset(HsHostingAssetEntity.builder().type(PGSQL_INSTANCE).build()) + .assignedToAsset(HsHostingAssetEntity.builder().type(PGSQL_INSTANCE).build()) + .build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenPgSqlUserHostingAsset.getType()); + + // when + final var result = validator.validateEntity(givenPgSqlUserHostingAsset); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'PGSQL_DATABASE:xyz00_temp.config.unknown' is not expected but is set to 'wrong'", + "'PGSQL_DATABASE:xyz00_temp.config.encoding' is expected to be of type String, but is of type Integer" + ); + } + @Test void rejectsInvalidProperties() { // given @@ -112,6 +133,6 @@ class HsPostgreSqlDatabaseHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'"); + "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9_]+$', but is 'xyz99-temp'"); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidatorUnitTest.java similarity index 73% rename from src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidatorUnitTest.java index 0886bb26..0875ea7b 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaPostgreSqlHostingAssetValidatorUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsPostgreSqlUserHostingAssetValidatorUnitTest.java @@ -1,36 +1,39 @@ package net.hostsharing.hsadminng.hs.hosting.asset.validators; +import net.hostsharing.hsadminng.hash.HashGenerator; 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.nio.charset.Charset; +import java.util.Base64; 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.HsHostingAssetType.PGSQL_INSTANCE; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_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 HsMariaPostgreSqlHostingAssetValidatorUnitTest { +class HsPostgreSqlUserHostingAssetValidatorUnitTest { - private static final HsHostingAssetEntity GIVEN_MARIADB_INSTANCE = HsHostingAssetEntity.builder() - .type(MARIADB_INSTANCE) + private static final HsHostingAssetEntity GIVEN_PGSQL_INSTANCE = HsHostingAssetEntity.builder() + .type(PGSQL_INSTANCE) .parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET) - .identifier("vm1234|MariaDB.default") - .caption("some valid test MariaDB-Instance") + .identifier("vm1234|PgSql.default") + .caption("some valid test PgSql-Instance") .build(); private static HsHostingAssetEntityBuilder givenValidMariaDbUserBuilder() { return HsHostingAssetEntity.builder() - .type(MARIADB_USER) + .type(PGSQL_USER) .parentAsset(TEST_MANAGED_WEBSPACE_HOSTING_ASSET) - .assignedToAsset(GIVEN_MARIADB_INSTANCE) + .assignedToAsset(GIVEN_PGSQL_INSTANCE) .identifier("xyz00_temp") - .caption("some valid test MariaDB-User") + .caption("some valid test PgSql-User") .config(new HashMap<>(ofEntries( entry("password", "Test1234") ))); @@ -46,7 +49,7 @@ class HsMariaPostgreSqlHostingAssetValidatorUnitTest { // then assertThat(props).extracting(Object::toString).containsExactlyInAnyOrder( - "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=MYSQL_NATIVE, undisclosed=true}" + "{type=password, propertyName=password, minLength=8, maxLength=40, writeOnly=true, computed=true, hashedUsing=SCRAM_SHA256, undisclosed=true}" ); } @@ -57,12 +60,12 @@ class HsMariaPostgreSqlHostingAssetValidatorUnitTest { final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType()); // when - // HashGenerator.nextSalt("Ly3LbsArtL5u4EVt"); // not needed for mysql_native_password + HashGenerator.nextSalt(new String(Base64.getDecoder().decode("L1QxSVNyTU81b3NZS1djNg=="), Charset.forName("latin1"))); validator.prepareProperties(givenMariaDbUserHostingAsset); // then assertThat(givenMariaDbUserHostingAsset.getConfig()).containsExactlyInAnyOrderEntriesOf(ofEntries( - entry("password", "*14F1A8C42F8B6D4662BB3ED290FD37BF135FE45C") + entry("password", "SCRAM-SHA-256$4096:L1QxSVNyTU81b3NZS1djNg==$bB4PEqHpnkoB9FwYfOjh+8yJvLsCnrwxom3TGK0CVJM=:ACRgTfhJwIZLrzhVRbJ3Qif5YhErYWAfkBThvtouW+8=") )); } @@ -98,9 +101,9 @@ class HsMariaPostgreSqlHostingAssetValidatorUnitTest { // 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" + "'PGSQL_USER:xyz00_temp.config.unknown' is not expected but is set to '100'", + "'PGSQL_USER:xyz00_temp.config.password' length is expected to be at min 8 but length of provided value is 5", + "'PGSQL_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" ); } @@ -117,6 +120,6 @@ class HsMariaPostgreSqlHostingAssetValidatorUnitTest { // then assertThat(result).containsExactly( - "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9]+$', but is 'xyz99-temp'"); + "'identifier' expected to match '^xyz00$|^xyz00_[a-z0-9_]+$', but is 'xyz99-temp'"); } } -- 2.39.5