diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java index aa4657b4..bf93e19f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HostingAssetEntityValidatorRegistry.java @@ -27,6 +27,7 @@ public class HostingAssetEntityValidatorRegistry { register(DOMAIN_MBOX_SETUP, new HsDomainMboxSetupHostingAssetValidator()); register(EMAIL_ADDRESS, new HsEMailAddressHostingAssetValidator()); register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator()); + register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { 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 new file mode 100644 index 00000000..778bd7ba --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsMariaDbUserHostingAssetValidator.java @@ -0,0 +1,33 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator; +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(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly()); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + final var webspaceIdentifier = assetEntity.getAssignedToAsset().getIdentifier(); + return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"_[a-z0-9]+$"); + } +} 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 70f102c2..ece84d9c 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 @@ -76,8 +76,8 @@ begin when 'PGSQL_USER' then 'MANAGED_WEBSPACE' when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE' when 'MARIADB_INSTANCE' then 'MANAGED_SERVER' - when 'MARIADB_USER' then 'MANAGED_WEBSPACE' - when 'MARIADB_DATABASE' then 'MANAGED_WEBSPACE' + when 'MARIADB_USER' then 'MARIADB_INSTANCE' + when 'MARIADB_DATABASE' then 'MARIADB_INSTANCE' 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 0d4b6a91..efc76846 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 @@ -77,18 +77,19 @@ begin 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), - (uuid_generate_v4(), cloudServerBI.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb), - (mariaDbInstanceUuid, null, 'MARIADB_INSTANCE', managedServerUuid, null, 'vm10' || debitorNumberSuffix || '.MariaDB.default','some default MariaDB instance','{}'::jsonb), - (managedWebspaceUuid, managedWebspaceBI.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::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), - (uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org|DNS', 'some Domain-DNS-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org|HTTP', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb), - (uuid_generate_v4(), null, 'DOMAIN_SMTP_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-SMPT-Setup', '{}'::jsonb), - (domainMBoxSetupUuid, null, 'DOMAIN_MBOX_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-MBOX-Setup', '{}'::jsonb), - (uuid_generate_v4(), null, 'EMAIL_ADDRESS', domainMBoxSetupUuid, null, 'test@' || defaultPrefix || '.example.org', 'some E-Mail-Address', '{}'::jsonb); + (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), + (mariaDbInstanceUuid, null, 'MARIADB_INSTANCE', managedServerUuid, null, 'vm10' || debitorNumberSuffix || '.MariaDB.default', 'some default MariaDB instance','{}'::jsonb), + (uuid_generate_v4(), null, 'MARIADB_USER', mariaDbInstanceUuid, null, defaultPrefix || '01_web', 'some default MariaDB user', '{ "password": "(ofEntries( + entry("password", "Hallo Datenbank, lass mich rein!") + ))); + } + + @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=SHA512, undisclosed=true}" + ); + } + + @Test + void preparesEntity() { + // given + final var givenMariaDbUserHostingAsset = givenValidMariaDbUserBuilder().build(); + final var validator = HostingAssetEntityValidatorRegistry.forType(givenMariaDbUserHostingAsset.getType()); + + // when + LinuxEtcShadowHashGenerator.nextSalt("Ly3LbsArtL5u4EVt"); // FIXME + validator.prepareProperties(givenMariaDbUserHostingAsset); + + // then + assertThat(givenMariaDbUserHostingAsset.getConfig()).containsExactlyInAnyOrderEntriesOf(ofEntries( + entry("password", "$6$Ly3LbsArtL5u4EVt$T7VM5uCq7I1cKttipCX4TQdyawdpLcSmjApreI4fZcORPOkEkxy9iz.9Dri6IVbO08OKTR8OE8hvnblU5Ax6o.") + )); + } + + @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'"); + } +}