add postgresql instance, database and user validators

This commit is contained in:
Michael Hoennig 2024-07-13 06:36:46 +02:00
parent 29994a851a
commit 797db51aec
22 changed files with 577 additions and 49 deletions

View File

@ -32,7 +32,7 @@ public final class HashGenerator {
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"), LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"), LINUX_YESCRYPT(LinuxEtcShadowHashGenerator::hash, "y"),
MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"), MYSQL_NATIVE(MySQLNativePasswordHashGenerator::hash, "*"),
SCRAM_SHA256(PostgreSQLScramSHA256::hash, "*"); SCRAM_SHA256(PostgreSQLScramSHA256::hash, "SCRAM-SHA-256");
final BiFunction<HashGenerator, String, String> implementation; final BiFunction<HashGenerator, String, String> implementation;
final String prefix; final String prefix;

View File

@ -82,17 +82,17 @@ public enum HsHostingAssetType implements Node {
PGSQL_INSTANCE( // TODO.spec: identifier to be specified PGSQL_INSTANCE( // TODO.spec: identifier to be specified
inGroup("PostgreSQL"), inGroup("PostgreSQL"),
requiredParent(MANAGED_SERVER)), requiredParent(MANAGED_SERVER)), // TODO.spec: or MANAGED_WEBSPACE?
PGSQL_USER( // named e.g. xyz00_abc PGSQL_USER( // named e.g. xyz00_abc
inGroup("PostgreSQL"), inGroup("PostgreSQL"),
requiredParent(PGSQL_INSTANCE), requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner
assignedTo(MANAGED_WEBSPACE)), assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied
PGSQL_DATABASE( // named e.g. xyz00_abc PGSQL_DATABASE( // named e.g. xyz00_abc
inGroup("PostgreSQL"), inGroup("PostgreSQL"),
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or PGSQL_USER? requiredParent(PGSQL_USER), // thus, the PGSQL_USER_USER:Agent becomes RBAC owner
assignedTo(PGSQL_INSTANCE)), // TODO.spec: or swapping parent+assignedTo? assignedTo(PGSQL_INSTANCE)), // keep in mind: no RBAC grants implied
MARIADB_INSTANCE( // TODO.spec: identifier to be specified MARIADB_INSTANCE( // TODO.spec: identifier to be specified
inGroup("MariaDB"), inGroup("MariaDB"),
@ -101,12 +101,12 @@ public enum HsHostingAssetType implements Node {
MARIADB_USER( // named e.g. xyz00_abc MARIADB_USER( // named e.g. xyz00_abc
inGroup("MariaDB"), inGroup("MariaDB"),
requiredParent(MANAGED_WEBSPACE), // thus, the MANAGED_WEBSPACE:Agent becomes RBAC owner 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 MARIADB_DATABASE( // named e.g. xyz00_abc
inGroup("MariaDB"), inGroup("MariaDB"),
requiredParent(MARIADB_USER), // thus, the MARIADB_USER:Agent becomes RBAC owner 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( IP_NUMBER(
inGroup("Server"), inGroup("Server"),

View File

@ -32,7 +32,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHo
HostingAssetEntityValidator( HostingAssetEntityValidator(
final HsHostingAssetType assetType, final HsHostingAssetType assetType,
final AlarmContact alarmContactValidation, final AlarmContact alarmContactValidation, // hostmaster alert address is implicitly added where needed
final ValidatableProperty<?, ?>... properties) { final ValidatableProperty<?, ?>... properties) {
super(properties); super(properties);
this.bookingItemReferenceValidation = new ReferenceValidator<>( this.bookingItemReferenceValidation = new ReferenceValidator<>(
@ -213,6 +213,7 @@ public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHo
super(policy, HsHostingAssetEntity::getAlarmContact); super(policy, HsHostingAssetEntity::getAlarmContact);
} }
// hostmaster alert address is implicitly added where neccessary
static AlarmContact isOptional() { static AlarmContact isOptional() {
return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL); return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL);
} }

View File

@ -29,6 +29,9 @@ public class HostingAssetEntityValidatorRegistry {
register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator()); register(MARIADB_INSTANCE, new HsMariaDbInstanceHostingAssetValidator());
register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator()); register(MARIADB_USER, new HsMariaDbUserHostingAssetValidator());
register(MARIADB_DATABASE, new HsMariaDbDatabaseHostingAssetValidator()); register(MARIADB_DATABASE, new HsMariaDbDatabaseHostingAssetValidator());
register(PGSQL_INSTANCE, new HsPostgreSqlDbInstanceHostingAssetValidator());
register(PGSQL_USER, new HsPostgreSqlUserHostingAssetValidator());
register(PGSQL_DATABASE, new HsPostgreSqlDatabaseHostingAssetValidator());
} }
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) { private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -14,7 +14,7 @@ class HsManagedServerHostingAssetValidator extends HostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() { public HsManagedServerHostingAssetValidator() {
super( super(
MANAGED_SERVER, MANAGED_SERVER,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added AlarmContact.isOptional(),
// monitoring // monitoring
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92), integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),

View File

@ -10,7 +10,7 @@ class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator
public HsManagedWebspaceHostingAssetValidator() { public HsManagedWebspaceHostingAssetValidator() {
super( super(
MANAGED_WEBSPACE, MANAGED_WEBSPACE,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES); NO_EXTRA_PROPERTIES);
} }

View File

@ -14,7 +14,7 @@ class HsMariaDbInstanceHostingAssetValidator extends HostingAssetEntityValidator
public HsMariaDbInstanceHostingAssetValidator() { public HsMariaDbInstanceHostingAssetValidator() {
super( super(
MARIADB_INSTANCE, MARIADB_INSTANCE,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions NO_EXTRA_PROPERTIES); // TODO.spec: specify instance properties, e.g. installed extensions
} }

View File

@ -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]+$");
}
}

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.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));
}
}
}

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.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]+$");
}
}

View File

@ -17,6 +17,7 @@ components:
- DOMAIN_MBOX_SETUP - DOMAIN_MBOX_SETUP
- EMAIL_ALIAS - EMAIL_ALIAS
- EMAIL_ADDRESS - EMAIL_ADDRESS
- PGSQL_INSTANCE
- PGSQL_USER - PGSQL_USER
- PGSQL_DATABASE - PGSQL_DATABASE
- MARIADB_INSTANCE - MARIADB_INSTANCE

View File

@ -16,6 +16,7 @@ create type HsHostingAssetType as enum (
'DOMAIN_MBOX_SETUP', 'DOMAIN_MBOX_SETUP',
'EMAIL_ALIAS', 'EMAIL_ALIAS',
'EMAIL_ADDRESS', 'EMAIL_ADDRESS',
'PGSQL_INSTANCE',
'PGSQL_USER', 'PGSQL_USER',
'PGSQL_DATABASE', 'PGSQL_DATABASE',
'MARIADB_INSTANCE', 'MARIADB_INSTANCE',
@ -48,6 +49,9 @@ create table if not exists hs_hosting_asset
--changeset hosting-asset-TYPE-HIERARCHY-CHECK:1 endDelimiter:--// --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() create or replace function hs_hosting_asset_type_hierarchy_check_tf()
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
@ -73,11 +77,14 @@ begin
when 'DOMAIN_SMTP_SETUP' then 'DOMAIN_SETUP' when 'DOMAIN_SMTP_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_MBOX_SETUP' then 'DOMAIN_SETUP' when 'DOMAIN_MBOX_SETUP' then 'DOMAIN_SETUP'
when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP' when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP'
when 'PGSQL_INSTANCE' then 'MANAGED_SERVER'
when 'PGSQL_USER' then 'MANAGED_WEBSPACE' 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_INSTANCE' then 'MANAGED_SERVER'
when 'MARIADB_USER' then 'MARIADB_INSTANCE' when 'MARIADB_USER' then 'MANAGED_WEBSPACE'
when 'MARIADB_DATABASE' then 'MARIADB_INSTANCE' when 'MARIADB_DATABASE' then 'MARIADB_USER'
else raiseException(format('[400] unknown asset type %s', NEW.type::text)) else raiseException(format('[400] unknown asset type %s', NEW.type::text))
end); end);

View File

@ -27,6 +27,8 @@ declare
domainMBoxSetupUuid uuid; domainMBoxSetupUuid uuid;
mariaDbInstanceUuid uuid; mariaDbInstanceUuid uuid;
mariaDbUserUuid uuid; mariaDbUserUuid uuid;
pgSqlInstanceUuid uuid;
PgSqlUserUuid uuid;
begin begin
currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; currentTask := 'creating hosting-asset test-data ' || givenProjectCaption;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); 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 domainMBoxSetupUuid;
select uuid_generate_v4() into mariaDbInstanceUuid; select uuid_generate_v4() into mariaDbInstanceUuid;
select uuid_generate_v4() into mariaDbUserUuid; select uuid_generate_v4() into mariaDbUserUuid;
select uuid_generate_v4() into pgSqlInstanceUuid;
select uuid_generate_v4() into pgSqlUserUuid;
debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix;
defaultPrefix := relatedDebitor.defaultPrefix; defaultPrefix := relatedDebitor.defaultPrefix;
@ -83,8 +87,11 @@ begin
(uuid_generate_v4(), cloudServerBI.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::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), (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), (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 ), (mariaDbUserUuid, null, 'MARIADB_USER', managedWebspaceUuid, mariaDbInstanceUuid, 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, 'MARIADB_DATABASE', mariaDbUserUuid, mariaDbInstanceUuid, defaultPrefix || '01_web', 'some default MariaDB database','{ "encryption": "utf8", "collation": "utf8"}'::jsonb ),
(pgSqlInstanceUuid, null, 'PGSQL_INSTANCE', managedServerUuid, null, 'vm10' || debitorNumberSuffix || '.Postgresql.default', 'some default Postgresql instance','{}'::jsonb),
(PgSqlUserUuid, null, 'PGSQL_USER', managedWebspaceUuid, pgSqlInstanceUuid, defaultPrefix || '01_web', 'some default Postgresql user', '{ "password": "<TODO:replace-by-encrypted-postgresql-password"}'::jsonb ),
(uuid_generate_v4(), null, 'PGSQL_DATABASE', pgSqlUserUuid, pgSqlInstanceUuid, defaultPrefix || '01_web', 'some default Postgresql 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), (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), (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), (domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb),

View File

@ -18,17 +18,22 @@ class HashGeneratorUnitTest {
final String GIVEN_SALT = "0123456789abcdef"; final String GIVEN_SALT = "0123456789abcdef";
// generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above) // generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above)
final String GIVEN_LINUX_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/"; final String GIVEN_LINUX_GENERATED_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/";
final String GIVEN_LINUX_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7"; final String GIVEN_LINUX_GENERATED_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7";
// generated in PostgreSQL using:
// CREATE USER test WITH PASSWORD 'given password';
// SELECT rolname, rolpassword FROM pg_authid WHERE rolname = 'test';
final String GIVEN_POSTGRESQL_GENERATED_SCRAM_SHA256_HASH = "SCRAM-SHA-256$4096:m8M12fdSTsKH+ywthTx1Zw==$4vsB1OddRNdsej9NPAFh91MPdtbOPjkQ85LQZS5lV0Q=:NsVpQNx4Ic/8Sqj1dxfBzUAxyF4FCTMpIsI+bOZCTfA=";
@Test @Test
void verifiesLinuxPasswordAgainstSha512HashFromMkpasswd() { void verifiesLinuxPasswordAgainstSha512HashFromMkpasswd() {
LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_SHA512_HASH, GIVEN_PASSWORD); // throws exception if wrong LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_GENERATED_SHA512_HASH, GIVEN_PASSWORD); // throws exception if wrong
} }
@Test @Test
void verifiesLinuxPasswordAgainstYescryptHashFromMkpasswd() { void verifiesLinuxPasswordAgainstYescryptHashFromMkpasswd() {
LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_YESCRYPT_HASH, GIVEN_PASSWORD); // throws exception if wrong LinuxEtcShadowHashGenerator.verify(GIVEN_LINUX_GENERATED_YESCRYPT_HASH, GIVEN_PASSWORD); // throws exception if wrong
} }
@Test @Test
@ -60,29 +65,13 @@ class HashGeneratorUnitTest {
@Test @Test
void generatePostgreSqlScramPasswordHash() { void generatePostgreSqlScramPasswordHash() {
//final var salt = new String(Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ==")); // given the same salt, extracted from the hash as generated by PostgreSQL
final var postgresBase64Salt = Base64.getDecoder().decode("3gsrkV5e1VZweIFiKfvoeQ=="); final var postgresBase64Salt = Base64.getDecoder().decode(GIVEN_POSTGRESQL_GENERATED_SCRAM_SHA256_HASH.split("\\$")[1].split(":")[1]);
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( // when the hash is re-generated via Java
"SCRAM-SHA-256$4096:3gsrkV5e1VZweIFiKfvoeQ==$/8I29AMTJ+7W9ceeKhc5LsfTrTHF6/+m/qstv2h0kpo=:+MDwFXgAjHHSnlqgU5adOPtW0qpbFUMrYp59Xs7ns0U="); final var hash = HashGenerator.using(SCRAM_SHA256).withSalt(new String(postgresBase64Salt, Charset.forName("latin1"))).hash(GIVEN_PASSWORD);
}
// ALTER USER your_username WITH PASSWORD 'SCRAM-SHA-256$iterations:base64-encoded-salt$base64-encoded-stored-key$base64-encoded-server-key'; // then we are getting the same hash
assertThat(hash).isEqualTo(GIVEN_POSTGRESQL_GENERATED_SCRAM_SHA256_HASH);
@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);
} }
} }

View File

@ -426,6 +426,68 @@ public class HsHostingAssetControllerRestTest {
} }
} }
] ]
"""),
PGSQL_INSTANCE(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.PGSQL_INSTANCE)
.parentAsset(TEST_MANAGED_SERVER_HOSTING_ASSET)
.identifier("vm1234|PgSql.default")
.caption("some fake PgSql instance")
.build()),
"""
[
{
"type": "PGSQL_INSTANCE",
"identifier": "vm1234|PgSql.default",
"caption": "some fake PgSql instance",
"alarmContact": null,
"config": {}
}
]
"""),
PGSQL_USER(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.PGSQL_USER)
.identifier("xyz00_temp")
.caption("some fake PgSql user")
.build()),
"""
[
{
"type": "PGSQL_USER",
"identifier": "xyz00_temp",
"caption": "some fake PgSql user",
"alarmContact": null,
"config": {}
}
]
"""),
PGSQL_DATABASE(
List.of(
HsHostingAssetEntity.builder()
.type(HsHostingAssetType.PGSQL_DATABASE)
.identifier("xyz00_temp")
.caption("some fake PgSql database")
.config(Map.ofEntries(
entry("encoding", "latin1"),
entry("collation", "latin2")
))
.build()),
"""
[
{
"type": "PGSQL_DATABASE",
"identifier": "xyz00_temp",
"caption": "some fake PgSql database",
"alarmContact": null,
"config": {
"encoding": "latin1",
"collation": "latin2"
}
}
]
"""); """);
final HsHostingAssetType assetType; final HsHostingAssetType assetType;

View File

@ -44,7 +44,10 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"EMAIL_ADDRESS", "EMAIL_ADDRESS",
"MARIADB_INSTANCE", "MARIADB_INSTANCE",
"MARIADB_USER", "MARIADB_USER",
"MARIADB_DATABASE" "MARIADB_DATABASE",
"PGSQL_INSTANCE",
"PGSQL_USER",
"PGSQL_DATABASE"
] ]
""")); """));
// @formatter:on // @formatter:on

View File

@ -246,7 +246,8 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu
exactlyTheseAssetsAreReturned( exactlyTheseAssetsAreReturned(
result, 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)"); "HsHostingAssetEntity(MARIADB_INSTANCE, vm1012.MariaDB.default, some default MariaDB instance, MANAGED_SERVER:vm1012)",
"HsHostingAssetEntity(PGSQL_INSTANCE, vm1012.Postgresql.default, some default Postgresql instance, MANAGED_SERVER:vm1012)");
} }
} }

View File

@ -191,9 +191,9 @@ class HsHostingAssetTypeUnitTest {
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER
HA_PGSQL_USER *==> HA_PGSQL_INSTANCE HA_PGSQL_USER *==> HA_MANAGED_WEBSPACE
HA_PGSQL_USER o..> HA_MANAGED_WEBSPACE HA_PGSQL_USER o..> HA_PGSQL_INSTANCE
HA_PGSQL_DATABASE *==> HA_MANAGED_WEBSPACE HA_PGSQL_DATABASE *==> HA_PGSQL_USER
HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE
HA_IP_NUMBER o..> HA_CLOUD_SERVER HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER HA_IP_NUMBER o..> HA_MANAGED_SERVER

View File

@ -42,7 +42,10 @@ class HostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.EMAIL_ADDRESS, HsHostingAssetType.EMAIL_ADDRESS,
HsHostingAssetType.MARIADB_INSTANCE, HsHostingAssetType.MARIADB_INSTANCE,
HsHostingAssetType.MARIADB_USER, HsHostingAssetType.MARIADB_USER,
HsHostingAssetType.MARIADB_DATABASE HsHostingAssetType.MARIADB_DATABASE,
HsHostingAssetType.PGSQL_INSTANCE,
HsHostingAssetType.PGSQL_USER,
HsHostingAssetType.PGSQL_DATABASE
); );
} }
} }

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 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'");
}
}

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.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'");
}
}

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 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'");
}
}