From f6d66d5712b7ffbe806c8e77f0b09e8e30862a61 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 5 Jul 2024 11:56:32 +0200 Subject: [PATCH] add-domain-setup-validation (#71) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/71 Reviewed-by: Marc Sandlus --- .../asset/HsHostingAssetController.java | 2 + .../hosting/asset/HsHostingAssetEntity.java | 10 + .../hs/hosting/asset/HsHostingAssetType.java | 7 +- ...HsDomainDnsSetupHostingAssetValidator.java | 106 ++++++++ .../HsDomainSetupHostingAssetValidator.java | 27 ++ .../HsHostingAssetEntityProcessor.java | 23 ++ ...HsHostingAssetEntityValidatorRegistry.java | 2 + .../hs/validation/HsEntityValidator.java | 31 +++ .../hsadminng/system/SystemProcess.java | 57 ++++ .../hs-hosting/hs-hosting-asset-schemas.yaml | 1 + .../7010-hs-hosting-asset.sql | 10 +- .../7013-hs-hosting-asset-rbac.md | 4 +- .../7013-hs-hosting-asset-rbac.sql | 115 +------- .../7018-hs-hosting-asset-test-data.sql | 6 +- .../HsHostingAssetControllerRestTest.java | 61 +++++ ...ingAssetPropsControllerAcceptanceTest.java | 4 +- ...HostingAssetRepositoryIntegrationTest.java | 36 ++- ...DnsSetupHostingAssetValidatorUnitTest.java | 245 ++++++++++++++++++ ...ainSetupHostingAssetValidatorUnitTest.java | 111 ++++++++ ...gAssetEntityValidatorRegistryUnitTest.java | 4 +- .../hsadminng/system/SystemProcessTest.java | 81 ++++++ 21 files changed, 821 insertions(+), 122 deletions(-) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java create mode 100644 src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java create mode 100644 src/test/java/net/hostsharing/hsadminng/system/SystemProcessTest.java diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java index d9b6492f..6e082c05 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetController.java @@ -73,6 +73,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); final var mapped = new HsHostingAssetEntityProcessor(entity) + .preprocessEntity() .validateEntity() .prepareForSave() .saveUsing(assetRepo::save) @@ -133,6 +134,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi { new HsHostingAssetEntityPatcher(em, entity).apply(body); final var mapped = new HsHostingAssetEntityProcessor(entity) + .preprocessEntity() .validateEntity() .prepareForSave() .saveUsing(assetRepo::save) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java index ae181921..80f9294c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetEntity.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.UUID; import static java.util.Collections.emptyMap; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL; @@ -51,6 +52,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.GUEST; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.REFERRER; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT; @@ -199,6 +201,13 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti directlyFetchedByDependsOnColumn(), NULLABLE) + .switchOnColumn("type", + inCaseOf("DOMAIN_SETUP", then -> { + then.toRole(GLOBAL, GUEST).grantPermission(INSERT); + then.toRole(GLOBAL, ADMIN).grantPermission(SELECT); // TODO.spec: replace by a proper solution + }) + ) + .createRole(OWNER, (with) -> { with.incomingSuperRole("bookingItem", ADMIN); with.incomingSuperRole("parentAsset", ADMIN); @@ -219,6 +228,7 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Properti with.incomingSuperRole("alarmContact", ADMIN); with.permission(SELECT); }) + .limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "alarmContact", "global"); } 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 f02a50f0..88ccca45 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 @@ -6,9 +6,10 @@ public enum HsHostingAssetType { MANAGED_SERVER, // named e.g. vm1234 MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00 UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc - DOMAIN_DNS_SETUP(MANAGED_WEBSPACE), // named e.g. example.org - DOMAIN_HTTP_SETUP(MANAGED_WEBSPACE), // named e.g. example.org - DOMAIN_EMAIL_SETUP(MANAGED_WEBSPACE), // named e.g. example.org + DOMAIN_SETUP, // named e.g. example.org + DOMAIN_DNS_SETUP(DOMAIN_SETUP), // named e.g. example.org + DOMAIN_HTTP_SETUP(DOMAIN_SETUP), // named e.g. example.org + DOMAIN_EMAIL_SETUP(DOMAIN_SETUP), // named e.g. example.org // TODO.spec: SECURE_MX EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java new file mode 100644 index 00000000..e09f77ef --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidator.java @@ -0,0 +1,106 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType; +import net.hostsharing.hsadminng.system.SystemProcess; + +import java.util.List; +import java.util.regex.Pattern; + +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf; +import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty; +import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty; +import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty; + +class HsDomainDnsSetupHostingAssetValidator extends HsHostingAssetEntityValidator { + + // according to RFC 1035 (section 5) and RFC 1034 + static final String RR_REGEX_NAME = "([a-z0-9\\.-]+|@)\\s+"; + static final String RR_REGEX_TTL = "(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*"; + static final String RR_REGEX_IN = "IN\\s+"; // record class IN for Internet + static final String RR_RECORD_TYPE = "[A-Z]+\\s+"; + static final String RR_RECORD_DATA = "[^;].*"; + static final String RR_COMMENT = "(;.*)*"; + + static final String RR_REGEX_TTL_IN = + RR_REGEX_NAME + RR_REGEX_TTL + RR_REGEX_IN + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; + + static final String RR_REGEX_IN_TTL = + RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT; + + HsDomainDnsSetupHostingAssetValidator() { + super( BookingItem.mustBeNull(), + ParentAsset.mustBeOfType(HsHostingAssetType.DOMAIN_SETUP), + AssignedToAsset.mustBeNull(), + AlarmContact.isOptional(), + + integerProperty("TTL").min(0).withDefault(21600), + booleanProperty("auto-SOA-RR").withDefault(true), + booleanProperty("auto-NS-RR").withDefault(true), + booleanProperty("auto-MX-RR").withDefault(true), + booleanProperty("auto-A-RR").withDefault(true), + booleanProperty("auto-AAAA-RR").withDefault(true), + booleanProperty("auto-MAILSERVICES-RR").withDefault(true), + booleanProperty("auto-AUTOCONFIG-RR").withDefault(true), // TODO.spec: does that already exist? + booleanProperty("auto-AUTODISCOVER-RR").withDefault(true), + booleanProperty("auto-DKIM-RR").withDefault(true), + booleanProperty("auto-SPF-RR").withDefault(true), + booleanProperty("auto-WILDCARD-MX-RR").withDefault(true), + booleanProperty("auto-WILDCARD-A-RR").withDefault(true), + booleanProperty("auto-WILDCARD-AAAA-RR").withDefault(true), + booleanProperty("auto-WILDCARD-DKIM-RR").withDefault(true), // TODO.spec: check, if that really works + booleanProperty("auto-WILDCARD-SPF-RR").withDefault(true), + arrayOf( + stringProperty("user-RR").matchesRegEx(RR_REGEX_TTL_IN, RR_REGEX_IN_TTL).required() + ).optional()); + } + + @Override + protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) { + return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + "$"); + } + + @Override + public void preprocessEntity(final HsHostingAssetEntity entity) { + super.preprocessEntity(entity); + if (entity.getIdentifier() == null) { + ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier())); + } + } + + @Override + @SneakyThrows + public List validateContext(final HsHostingAssetEntity assetEntity) { + final var result = super.validateContext(assetEntity); + + // TODO.spec: define which checks should get raised to error level + final var namedCheckZone = new SystemProcess("named-checkzone", assetEntity.getIdentifier()); + if (namedCheckZone.execute(toZonefileString(assetEntity)) != 0) { + // yes, named-checkzone writes error messages to stdout + stream(namedCheckZone.getStdOut().split("\n")) + .map(line -> line.replaceAll(" stream-0x[0-9a-f:]+", "")) + .forEach(result::add); + } + return result; + } + + String toZonefileString(final HsHostingAssetEntity assetEntity) { + // TODO.spec: we need to expand the templates (auto-...) in the same way as in Saltstack + return """ + $ORIGIN {domain}. + $TTL {ttl} + + ; these records are just placeholders to create a valid zonefile for the validation + @ 1814400 IN SOA {domain}. root.{domain} ( 1999010100 10800 900 604800 86400 ) + @ IN NS ns + + {userRRs} + """ + .replace("{domain}", assetEntity.getIdentifier()) + .replace("{ttl}", getPropertyValue(assetEntity, "TTL")) + .replace("{userRRs}", getPropertyValues(assetEntity, "user-RR") ); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java new file mode 100644 index 00000000..d2693f7e --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidator.java @@ -0,0 +1,27 @@ +package net.hostsharing.hsadminng.hs.hosting.asset.validators; + +import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity; + +import java.util.regex.Pattern; + +class HsDomainSetupHostingAssetValidator extends HsHostingAssetEntityValidator { + + public static final String DOMAIN_NAME_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(? validator; + private String expectedStep = "preprocessEntity"; private HsHostingAssetEntity entity; private HsHostingAssetResource resource; @@ -22,8 +23,16 @@ public class HsHostingAssetEntityProcessor { this.validator = HsHostingAssetEntityValidatorRegistry.forType(entity.getType()); } + /// initial step allowing to set default values before any validations + public HsHostingAssetEntityProcessor preprocessEntity() { + step("preprocessEntity", "validateEntity"); + validator.preprocessEntity(entity); + return this; + } + /// validates the entity itself including its properties public HsHostingAssetEntityProcessor validateEntity() { + step("validateEntity", "prepareForSave"); MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity)); return this; } @@ -31,17 +40,20 @@ public class HsHostingAssetEntityProcessor { /// hashing passwords etc. @SuppressWarnings("unchecked") public HsHostingAssetEntityProcessor prepareForSave() { + step("prepareForSave", "saveUsing"); validator.prepareProperties(entity); return this; } public HsHostingAssetEntityProcessor saveUsing(final Function saveFunction) { + step("saveUsing", "validateContext"); entity = saveFunction.apply(entity); return this; } /// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits) public HsHostingAssetEntityProcessor validateContext() { + step("validateContext", "mapUsing"); MultiValidationException.throwIfNotEmpty(validator.validateContext(entity)); return this; } @@ -49,6 +61,7 @@ public class HsHostingAssetEntityProcessor { /// maps entity to JSON resource representation public HsHostingAssetEntityProcessor mapUsing( final Function mapFunction) { + step("mapUsing", "revampProperties"); resource = mapFunction.apply(entity); return this; } @@ -56,8 +69,18 @@ public class HsHostingAssetEntityProcessor { /// removes write-only-properties and ads computed-properties @SuppressWarnings("unchecked") public HsHostingAssetResource revampProperties() { + step("revampProperties", null); final var revampedProps = validator.revampProperties(entity, (Map) resource.getConfig()); resource.setConfig(revampedProps); return resource; } + + // Makes sure that the steps are called in the correct order. + // Could also be implemented using an interface per method, but that seems exaggerated. + private void step(final String current, final String next) { + if (!expectedStep.equals(current)) { + throw new IllegalStateException("expected " + expectedStep + " but got " + current); + } + expectedStep = next; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java index a30108e7..3ae14256 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsHostingAssetEntityValidatorRegistry.java @@ -20,6 +20,8 @@ public class HsHostingAssetEntityValidatorRegistry { register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); register(UNIX_USER, new HsUnixUserHostingAssetValidator()); register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); + register(DOMAIN_SETUP, new HsDomainSetupHostingAssetValidator()); + register(DOMAIN_DNS_SETUP, new HsDomainDnsSetupHostingAssetValidator()); } private static void register(final Enum type, final HsEntityValidator validator) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java index 13cb3f05..de4b70bc 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/HsEntityValidator.java @@ -6,7 +6,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Collectors; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; @@ -41,6 +43,19 @@ public abstract class HsEntityValidator { .toList(); } + public final Map> propertiesMap() { + return Arrays.stream(propertyValidators) + .map(ValidatableProperty::toOrderedMap) + .collect(Collectors.toMap(p -> p.get("propertyName").toString(), p -> p)); + } + + /** + Gets called before any validations take place. + Allows to initialize fields and properties to default values. + */ + public void preprocessEntity(final E entity) { + } + protected ArrayList validateProperties(final PropertiesProvider propsProvider) { final var result = new ArrayList(); @@ -109,4 +124,20 @@ public abstract class HsEntityValidator { }); return copy; } + + protected String getPropertyValue(final PropertiesProvider entity, final String propertyName) { + final var rawValue = entity.getDirectValue(propertyName, Object.class); + if (rawValue != null) { + return rawValue.toString(); + } + return Objects.toString(propertiesMap().get(propertyName).get("defaultValue")); + } + + protected String getPropertyValues(final PropertiesProvider entity, final String propertyName) { + final var rawValue = entity.getDirectValue(propertyName, Object[].class); + if (rawValue != null) { + return stream(rawValue).map(Object::toString).collect(Collectors.joining("\n")); + } + return ""; + } } diff --git a/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java b/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java new file mode 100644 index 00000000..149c6019 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/system/SystemProcess.java @@ -0,0 +1,57 @@ +package net.hostsharing.hsadminng.system; + +import lombok.Getter; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; + +public class SystemProcess { + private final ProcessBuilder processBuilder; + + @Getter + private String stdOut; + @Getter + private String stdErr; + + public SystemProcess(final String... command) { + this.processBuilder = new ProcessBuilder(command); + } + + public int execute() throws IOException, InterruptedException { + final var process = processBuilder.start(); + stdOut = fetchOutput(process.getInputStream()); // yeah, twisted ProcessBuilder API + stdErr = fetchOutput(process.getErrorStream()); + return process.waitFor(); + } + + public int execute(final String input) throws IOException, InterruptedException { + final var process = processBuilder.start(); + feedInput(input, process); + stdOut = fetchOutput(process.getInputStream()); // yeah, twisted ProcessBuilder API + stdErr = fetchOutput(process.getErrorStream()); + return process.waitFor(); + } + + private static void feedInput(final String input, final Process process) throws IOException { + try ( + final OutputStreamWriter stdIn = new OutputStreamWriter(process.getOutputStream()); // yeah, twisted ProcessBuilder API + final BufferedWriter writer = new BufferedWriter(stdIn)) { + writer.write(input); + writer.flush(); + } + } + + private static String fetchOutput(final InputStream inputStream) throws IOException { + final var output = new StringBuilder(); + try (final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + for (String line; (line = reader.readLine()) != null; ) { + output.append(line).append(System.lineSeparator()); + } + } + return output.toString(); + } +} 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 934c9647..a9ab7f64 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 @@ -10,6 +10,7 @@ components: - MANAGED_SERVER - MANAGED_WEBSPACE - UNIX_USER + - DOMAIN_SETUP - DOMAIN_DNS_SETUP - DOMAIN_HTTP_SETUP - DOMAIN_EMAIL_SETUP 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 bd6ff6e4..eb335238 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 @@ -9,6 +9,7 @@ create type HsHostingAssetType as enum ( 'MANAGED_SERVER', 'MANAGED_WEBSPACE', 'UNIX_USER', + 'DOMAIN_SETUP', 'DOMAIN_DNS_SETUP', 'DOMAIN_HTTP_SETUP', 'DOMAIN_EMAIL_SETUP', @@ -36,7 +37,7 @@ create table if not exists hs_hosting_asset alarmContactUuid uuid null references hs_office_contact(uuid) initially deferred, constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset - check (bookingItemUuid is not null or parentAssetUuid is not null) + check (bookingItemUuid is not null or parentAssetUuid is not null or type='DOMAIN_SETUP') ); --// @@ -63,9 +64,10 @@ begin when 'MANAGED_SERVER' then null when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER' when 'UNIX_USER' then 'MANAGED_WEBSPACE' - when 'DOMAIN_DNS_SETUP' then 'MANAGED_WEBSPACE' - when 'DOMAIN_HTTP_SETUP' then 'MANAGED_WEBSPACE' - when 'DOMAIN_EMAIL_SETUP' then 'MANAGED_WEBSPACE' + when 'DOMAIN_SETUP' then null + when 'DOMAIN_DNS_SETUP' then 'DOMAIN_SETUP' + when 'DOMAIN_HTTP_SETUP' then 'DOMAIN_SETUP' + when 'DOMAIN_EMAIL_SETUP' then 'DOMAIN_SETUP' when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP' when 'PGSQL_USER' then 'MANAGED_WEBSPACE' diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md index f0b250db..37b47e15 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.md @@ -36,9 +36,9 @@ subgraph asset["`**asset**`"] style asset:permissions fill:#dd4901,stroke:white perm:asset:INSERT{{asset:INSERT}} + perm:asset:SELECT{{asset:SELECT}} perm:asset:DELETE{{asset:DELETE}} perm:asset:UPDATE{{asset:UPDATE}} - perm:asset:SELECT{{asset:SELECT}} end end @@ -103,6 +103,8 @@ role:alarmContact:ADMIN ==> role:asset:TENANT %% granting permissions to roles role:global:ADMIN ==> perm:asset:INSERT role:parentAsset:ADMIN ==> perm:asset:INSERT +role:global:GUEST ==> perm:asset:INSERT +role:global:ADMIN ==> perm:asset:SELECT role:asset:OWNER ==> perm:asset:DELETE role:asset:ADMIN ==> perm:asset:UPDATE role:asset:TENANT ==> perm:asset:SELECT diff --git a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql index cbaffa47..5b740226 100644 --- a/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql +++ b/src/main/resources/db/changelog/7-hs-hosting/701-hosting-asset/7013-hs-hosting-asset-rbac.sql @@ -82,6 +82,13 @@ begin hsHostingAssetTENANT(newParentAsset)] ); + IF NEW.type = 'DOMAIN_SETUP' THEN + END IF; + + + + call grantPermissionToRole(createPermission(NEW.uuid, 'SELECT'), globalAdmin()); + call leaveTriggerForObjectUuid(NEW.uuid); end; $$; @@ -147,114 +154,6 @@ execute procedure updateTriggerForHsHostingAsset_tf(); --// --- ============================================================================ ---changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--// --- ---------------------------------------------------------------------------- - --- granting INSERT permission to global ---------------------------- - -/* - Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing global rows. - */ -do language plpgsql $$ - declare - row global; - begin - call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising global rows'); - - FOR row IN SELECT * FROM global - -- unconditional for all rows in that table - LOOP - call grantPermissionToRole( - createPermission(row.uuid, 'INSERT', 'hs_hosting_asset'), - globalADMIN()); - END LOOP; - end; -$$; - -/** - Grants hs_hosting_asset INSERT permission to specified role of new global rows. -*/ -create or replace function new_hs_hosting_asset_grants_insert_to_global_tf() - returns trigger - language plpgsql - strict as $$ -begin - -- unconditional for all rows in that table - call grantPermissionToRole( - createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), - globalADMIN()); - -- end. - return NEW; -end; $$; - --- z_... is to put it at the end of after insert triggers, to make sure the roles exist -create trigger z_new_hs_hosting_asset_grants_insert_to_global_tg - after insert on global - for each row -execute procedure new_hs_hosting_asset_grants_insert_to_global_tf(); - --- granting INSERT permission to hs_hosting_asset ---------------------------- - --- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped, --- because there cannot yet be any pre-existing rows in the same table yet. - -/** - Grants hs_hosting_asset INSERT permission to specified role of new hs_hosting_asset rows. -*/ -create or replace function new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf() - returns trigger - language plpgsql - strict as $$ -begin - -- unconditional for all rows in that table - call grantPermissionToRole( - createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'), - hsHostingAssetADMIN(NEW)); - -- end. - return NEW; -end; $$; - --- z_... is to put it at the end of after insert triggers, to make sure the roles exist -create trigger z_new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tg - after insert on hs_hosting_asset - for each row -execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf(); - - --- ============================================================================ ---changeset hs_hosting_asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--// --- ---------------------------------------------------------------------------- - -/** - Checks if the user respectively the assumed roles are allowed to insert a row to hs_hosting_asset. -*/ -create or replace function hs_hosting_asset_insert_permission_check_tf() - returns trigger - language plpgsql as $$ -declare - superObjectUuid uuid; -begin - -- check INSERT INSERT if global ADMIN - if isGlobalAdmin() then - return NEW; - end if; - -- check INSERT permission via direct foreign key: NEW.parentAssetUuid - if hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then - return NEW; - end if; - - raise exception '[403] insert into hs_hosting_asset values(%) not allowed for current subjects % (%)', - NEW, currentSubjects(), currentSubjectsUuids(); -end; $$; - -create trigger hs_hosting_asset_insert_permission_check_tg - before insert on hs_hosting_asset - for each row - execute procedure hs_hosting_asset_insert_permission_check_tf(); ---// - - -- ============================================================================ --changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- 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 32f2804a..736c129d 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 @@ -23,6 +23,7 @@ declare managedServerUuid uuid; managedWebspaceUuid uuid; webUnixUserUuid uuid; + domainSetupUuid uuid; begin currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); @@ -65,6 +66,7 @@ begin select uuid_generate_v4() into managedServerUuid; select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into webUnixUserUuid; + select uuid_generate_v4() into domainSetupUuid; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; defaultPrefix := relatedDebitor.defaultPrefix; @@ -75,7 +77,9 @@ begin (managedWebspaceUuid, relatedManagedWebspaceBookingItem.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), - (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', managedWebspaceUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::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', 'some Domain-DNS-Setup', '{}'::jsonb), + (uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb); end; $$; --// diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java index 529d34cd..eed85585 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetControllerRestTest.java @@ -185,6 +185,67 @@ public class HsHostingAssetControllerRestTest { } } ] + """), + DOMAIN_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_SETUP) + .identifier("example.org") + .caption("some fake Domain-Setup") + .build()), + """ + [ + { + "type": "DOMAIN_SETUP", + "identifier": "example.org", + "caption": "some fake Domain-Setup", + "alarmContact": null, + "config": {} + } + ] + """), + DOMAIN_DNS_SETUP( + List.of( + HsHostingAssetEntity.builder() + .type(HsHostingAssetType.DOMAIN_DNS_SETUP) + .identifier("example.org") + .caption("some fake Domain-DNS-Setup") + .config(Map.ofEntries( + entry("auto-WILDCARD-MX-RR", false), + entry("auto-WILDCARD-A-RR", false), + entry("auto-WILDCARD-AAAA-RR", false), + entry("auto-WILDCARD-DKIM-RR", false), + entry("auto-WILDCARD-SPF-RR", false), + entry("user-RR", Array.of( + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com") + ) + )) + .build()), + """ + [ + { + "type": "DOMAIN_DNS_SETUP", + "identifier": "example.org", + "caption": "some fake Domain-DNS-Setup", + "alarmContact": null, + "config": { + "auto-WILDCARD-AAAA-RR": false, + "auto-WILDCARD-MX-RR": false, + "auto-WILDCARD-SPF-RR": false, + "auto-WILDCARD-DKIM-RR": false, + "auto-WILDCARD-A-RR": false, + "user-RR": [ + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com" + ] + } + } + ] """); final HsHostingAssetType assetType; diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java index e8323839..bd571075 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetPropsControllerAcceptanceTest.java @@ -35,7 +35,9 @@ class HsHostingAssetPropsControllerAcceptanceTest { "MANAGED_WEBSPACE", "CLOUD_SERVER", "UNIX_USER", - "EMAIL_ALIAS" + "EMAIL_ALIAS", + "DOMAIN_SETUP", + "DOMAIN_DNS_SETUP" ] """)); // @formatter:on diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java index cc8a029b..579257a0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/HsHostingAssetRepositoryIntegrationTest.java @@ -27,6 +27,7 @@ import java.util.Map; import static java.util.Map.entry; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER; import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE; import static net.hostsharing.hsadminng.rbac.rbacgrant.RawRbacGrantEntity.distinctGrantDisplaysOf; @@ -129,6 +130,9 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu .containsExactlyInAnyOrder(fromFormatted( initialGrantNames, + // global-admin + "{ grant perm:hs_hosting_asset#fir00:SELECT to role:global#global:ADMIN by system and assume }", // workaround + // owner "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_booking_item#fir01:ADMIN by system and assume }", "{ grant role:hs_hosting_asset#fir00:OWNER to role:hs_hosting_asset#vm1011:ADMIN by system and assume }", @@ -137,7 +141,6 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu // admin "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_hosting_asset#fir00:OWNER by system and assume }", "{ grant role:hs_hosting_asset#fir00:ADMIN to role:hs_booking_item#fir01:AGENT by system and assume }", - "{ grant perm:hs_hosting_asset#fir00:INSERT>hs_hosting_asset to role:hs_hosting_asset#fir00:ADMIN by system and assume }", "{ grant perm:hs_hosting_asset#fir00:UPDATE to role:hs_hosting_asset#fir00:ADMIN by system and assume }", // agent @@ -148,17 +151,44 @@ class HsHostingAssetRepositoryIntegrationTest extends ContextBasedTestWithCleanu "{ grant role:hs_booking_item#fir01:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", "{ grant role:hs_hosting_asset#fir00:TENANT to role:hs_hosting_asset#fir00:AGENT by system and assume }", "{ grant role:hs_hosting_asset#vm1011:TENANT to role:hs_hosting_asset#fir00:TENANT by system and assume }", - "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", + "{ grant perm:hs_hosting_asset#fir00:SELECT to role:hs_hosting_asset#fir00:TENANT by system and assume }", // workaround null)); } + @Test + public void anyUser_canCreateNewDomainSetupAsset() { + // given + context("superuser-alex@hostsharing.net"); + final var assetCount = assetRepo.count(); + + // when + context("person-SmithPeter@example.com"); + final var result = attempt(em, () -> { + final var newAsset = HsHostingAssetEntity.builder() + .caption("some new domain setup") + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + return toCleanup(assetRepo.save(newAsset)); + }); + + // then + result.assertSuccessful(); + assertThat(result.returnedValue()).isNotNull().extracting(HsHostingAssetEntity::getUuid).isNotNull(); + assertThat(result.returnedValue().isLoaded()).isFalse(); + context("superuser-alex@hostsharing.net"); + assertThatAssetIsPersisted(result.returnedValue()); + assertThat(assetRepo.count()).isEqualTo(assetCount + 1); + } + private void assertThatAssetIsPersisted(final HsHostingAssetEntity saved) { + final var context = attempt(em, () -> { - context("superuser-alex@hostsharing.net"); final var found = assetRepo.findByUuid(saved.getUuid()); assertThat(found).isNotEmpty().map(HsHostingAssetEntity::toString).get().isEqualTo(saved.toString()); }); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..671b9452 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainDnsSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,245 @@ +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 net.hostsharing.hsadminng.mapper.Array; +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_DNS_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_COMMENT; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_DATA; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_RECORD_TYPE; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_IN; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_NAME; +import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsDomainDnsSetupHostingAssetValidator.RR_REGEX_TTL; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainDnsSetupHostingAssetValidatorUnitTest { + + static final HsHostingAssetEntity validDomainSetupEntity = HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org") + .build(); + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_DNS_SETUP) + .parentAsset(validDomainSetupEntity) + .identifier("example.org") + .config(Map.ofEntries( + entry("user-RR", Array.of( + "@ 1814400 IN XXX example.org. root.example.org ( 1234 10800 900 604800 86400 )", + "www IN CNAME example.com. ; www.example.com is an alias for example.com", + "test1 IN 1h30m CNAME example.com.", + "test2 1h30m IN CNAME example.com.", + "ns IN A 192.0.2.2; IPv4 address for ns.example.com") + ) + )); + } + + @Test + void containsExpectedProperties() { + // when + final var validator = HsHostingAssetEntityValidatorRegistry.forType(DOMAIN_DNS_SETUP); + + // then + assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder( + "{type=integer, propertyName=TTL, min=0, defaultValue=21600}", + "{type=boolean, propertyName=auto-SOA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-NS-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-MX-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-A-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AAAA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-MAILSERVICES-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AUTOCONFIG-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-AUTODISCOVER-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-DKIM-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-SPF-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-MX-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-A-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-AAAA-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-DKIM-RR, defaultValue=true}", + "{type=boolean, propertyName=auto-WILDCARD-SPF-RR, defaultValue=true}", + "{type=string[], propertyName=user-RR, elementsOf={type=string, propertyName=user-RR, matchesRegEx=[([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*], required=true}}" + ); + } + + @Test + void preprocessesTakesIdentifierFromParent() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + validator.preprocessEntity(givenEntity); + + // then + assertThat(givenEntity.getIdentifier()).isEqualTo(givenEntity.getParentAsset().getIdentifier()); + } + + @Test + void rejectsInvalidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier("wrong.org").build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^example.org$', but is 'wrong.org'" + ); + } + + @Test + void acceptsValidIdentifier() { + // given + final var givenEntity = validEntityBuilder().identifier(validDomainSetupEntity.getIdentifier()).build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).isEmpty(); + } + + @Test + void validatesReferencedEntities() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .parentAsset(HsHostingAssetEntity.builder().build()) + .assignedToAsset(HsHostingAssetEntity.builder().build()) + .bookingItem(HsBookingItemEntity.builder().type(HsBookingItemType.CLOUD_SERVER).build()) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_DNS_SETUP:example.org.bookingItem' must be null but is set to D-???????-?:null", + "'DOMAIN_DNS_SETUP:example.org.parentAsset' must be of type DOMAIN_SETUP but is of type null", + "'DOMAIN_DNS_SETUP:example.org.assignedToAsset' must be null but is set to D-???????-?:null"); + } + + @Test + void acceptsValidEntity() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateEntity(givenEntity); + + // then + assertThat(errors).isEmpty(); + } + + @Test + void recectsInvalidProperties() { + // given + final var mangedServerHostingAssetEntity = validEntityBuilder() + .config(Map.ofEntries( + entry("TTL", "1d30m"), // currently only an integer for seconds is implemented here + entry("user-RR", Array.of( + "@ 1814400 IN 1814400 BAD1 TTL only allowed once", + "www BAD1 Record-Class missing / not enough columns")) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(mangedServerHostingAssetEntity.getType()); + + // when + final var result = validator.validateEntity(mangedServerHostingAssetEntity); + + // then + assertThat(result).containsExactlyInAnyOrder( + "'DOMAIN_DNS_SETUP:example.org.config.TTL' is expected to be of type class java.lang.Integer, but is of type 'String'", + "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but '@ 1814400 IN 1814400 BAD1 TTL only allowed once' does not match any", + "'DOMAIN_DNS_SETUP:example.org.config.user-RR' is expected to match any of [([a-z0-9\\.-]+|@)\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*IN\\s+[A-Z]+\\s+[^;].*(;.*)*, ([a-z0-9\\.-]+|@)\\s+IN\\s+(([1-9][0-9]*[mMhHdDwW]{0,1})+\\s+)*[A-Z]+\\s+[^;].*(;.*)*] but 'www BAD1 Record-Class missing / not enough columns' does not match any"); + } + + @Test + void validStringMatchesRegEx() { + assertThat("@ ").matches(RR_REGEX_NAME); + assertThat("ns ").matches(RR_REGEX_NAME); + assertThat("example.com. ").matches(RR_REGEX_NAME); + + assertThat("12400 ").matches(RR_REGEX_TTL); + assertThat("12400\t\t ").matches(RR_REGEX_TTL); + assertThat("12400 \t\t").matches(RR_REGEX_TTL); + assertThat("1h30m ").matches(RR_REGEX_TTL); + assertThat("30m ").matches(RR_REGEX_TTL); + + assertThat("IN ").matches(RR_REGEX_IN); + assertThat("IN\t\t ").matches(RR_REGEX_IN); + assertThat("IN \t\t").matches(RR_REGEX_IN); + + assertThat("CNAME ").matches(RR_RECORD_TYPE); + assertThat("CNAME\t\t ").matches(RR_RECORD_TYPE); + assertThat("CNAME \t\t").matches(RR_RECORD_TYPE); + + assertThat("example.com.").matches(RR_RECORD_DATA); + assertThat("123.123.123.123").matches(RR_RECORD_DATA); + assertThat("(some more complex argument in parenthesis)").matches(RR_RECORD_DATA); + assertThat("\"some more complex argument; including a semicolon\"").matches(RR_RECORD_DATA); + + assertThat("; whatever ; \" really anything").matches(RR_COMMENT); + } + + @Test + void generatesZonefile() { + // given + final var givenEntity = validEntityBuilder().build(); + final var validator = (HsDomainDnsSetupHostingAssetValidator) HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var zonefile = validator.toZonefileString(givenEntity); + + // then + assertThat(zonefile).isEqualTo(""" + $ORIGIN example.org. + $TTL 21600 + + ; these records are just placeholders to create a valid zonefile for the validation + @ 1814400 IN SOA example.org. root.example.org ( 1999010100 10800 900 604800 86400 ) + @ IN NS ns + + @ 1814400 IN XXX example.org. root.example.org ( 1234 10800 900 604800 86400 ) + www IN CNAME example.com. ; www.example.com is an alias for example.com + test1 IN 1h30m CNAME example.com. + test2 1h30m IN CNAME example.com. + ns IN A 192.0.2.2; IPv4 address for ns.example.com + """); + } + + @Test + void rejectsInvalidZonefile() { + // given + final var givenEntity = validEntityBuilder().config(Map.ofEntries( + entry("user-RR", Array.of( + "example.org. 1814400 IN SOA example.org. root.example.org (1234 10800 900 604800 86400)" + )) + )) + .build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var errors = validator.validateContext(givenEntity); + + // then + assertThat(errors).containsExactlyInAnyOrder( + "dns_master_load: example.org: multiple RRs of singleton type", + "zone example.org/IN: loading from master file (null) failed: multiple RRs of singleton type", + "zone example.org/IN: not loaded due to errors." + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java new file mode 100644 index 00000000..b7d78567 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/hosting/asset/validators/HsDomainSetupHostingAssetValidatorUnitTest.java @@ -0,0 +1,111 @@ +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 org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.Map; + +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER; +import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP; +import static org.assertj.core.api.Assertions.assertThat; + +class HsDomainSetupHostingAssetValidatorUnitTest { + + static HsHostingAssetEntityBuilder validEntityBuilder() { + return HsHostingAssetEntity.builder() + .type(DOMAIN_SETUP) + .identifier("example.org"); + } + + enum InvalidDomainNameIdentifier { + EMPTY(""), + TOO_LONG("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456890123456789.de"), + DASH_AT_BEGINNING("-example.com"), + DOT_AT_BEGINNING(".example.com"), + DOT_AT_END("example.com."); + + final String domainName; + + InvalidDomainNameIdentifier(final String domainName) { + this.domainName = domainName; + } + } + + @ParameterizedTest + @EnumSource(InvalidDomainNameIdentifier.class) + void rejectsInvalidIdentifier(final InvalidDomainNameIdentifier testCase) { + // given + final var givenEntity = validEntityBuilder().identifier(testCase.domainName).build(); + final var validator = HsHostingAssetEntityValidatorRegistry.forType(givenEntity.getType()); + + // when + final var result = validator.validateEntity(givenEntity); + + // then + assertThat(result).containsExactly( + "'identifier' expected to match '^((?!-)[A-Za-z0-9-]{1,63}(?&2"); + + // when + final var returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(0); + assertThat(process.getStdOut()).isEqualTo("Hello, World!\n"); + assertThat(process.getStdErr()).isEqualTo("Error!\n"); + } + + @Test + @EnabledOnOs(LINUX) + void shouldReturnErrorCode() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("false"); + + // when + final int returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(1); + } + + @Test + @EnabledOnOs(LINUX) + void shouldExecuteAndFeedInput() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("tr", "[:lower:]", "[:upper:]"); + + // when + final int returnCode = process.execute("Hallo"); + + // then + assertThat(returnCode).isEqualTo(0); + assertThat(process.getStdOut()).isEqualTo("HALLO\n"); + } + + @Test + void shouldThrowExceptionIfProgramNotFound() { + // given + final var process = new SystemProcess("non-existing program"); + + // when + final var exception = catchThrowable(process::execute); + + // then + assertThat(exception).isInstanceOf(IOException.class) + .hasMessage("Cannot run program \"non-existing program\": error=2, No such file or directory"); + } + + @Test + void shouldBeAbleToRunMultipleTimes() throws IOException, InterruptedException { + // given + final var process = new SystemProcess("true"); + + // when + process.execute(); + final int returnCode = process.execute(); + + // then + assertThat(returnCode).isEqualTo(0); + } +}