add-domain-setup-validation (#71)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #71
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-07-05 11:56:32 +02:00
parent a77eaefb94
commit f6d66d5712
21 changed files with 821 additions and 122 deletions

View File

@ -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)

View File

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

View File

@ -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

View File

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

View File

@ -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}(?<!-)\\.)+[A-Za-z]{2,6}";
private final Pattern identifierPattern;
HsDomainSetupHostingAssetValidator() {
super( BookingItem.mustBeNull(),
ParentAsset.mustBeNull(),
AssignedToAsset.mustBeNull(),
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
this.identifierPattern = Pattern.compile(DOMAIN_NAME_REGEX);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return identifierPattern;
}
}

View File

@ -14,6 +14,7 @@ import java.util.function.Function;
public class HsHostingAssetEntityProcessor {
private final HsEntityValidator<HsHostingAssetEntity> 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<HsHostingAssetEntity, HsHostingAssetEntity> 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<HsHostingAssetEntity, HsHostingAssetResource> 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<String, Object>) 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;
}
}

View File

@ -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<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

@ -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<E extends PropertiesProvider> {
.toList();
}
public final Map<String, Map<String, Object>> 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<String> validateProperties(final PropertiesProvider propsProvider) {
final var result = new ArrayList<String>();
@ -109,4 +124,20 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
});
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 "";
}
}

View File

@ -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();
}
}

View File

@ -10,6 +10,7 @@ components:
- MANAGED_SERVER
- MANAGED_WEBSPACE
- UNIX_USER
- DOMAIN_SETUP
- DOMAIN_DNS_SETUP
- DOMAIN_HTTP_SETUP
- DOMAIN_EMAIL_SETUP

View File

@ -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'

View File

@ -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

View File

@ -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:--//
-- ----------------------------------------------------------------------------

View File

@ -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; $$;
--//

View File

@ -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;

View File

@ -35,7 +35,9 @@ class HsHostingAssetPropsControllerAcceptanceTest {
"MANAGED_WEBSPACE",
"CLOUD_SERVER",
"UNIX_USER",
"EMAIL_ALIAS"
"EMAIL_ALIAS",
"DOMAIN_SETUP",
"DOMAIN_DNS_SETUP"
]
"""));
// @formatter:on

View File

@ -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());
});
}
}

View File

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

View File

@ -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}(?<!-)\\.)+[A-Za-z]{2,6}', but is '"+testCase.domainName+"'"
);
}
enum ValidDomainNameIdentifier {
SIMPLE("exampe.org"),
MAX_LENGTH("abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz01234568901.de"),
WITH_DASH("example-test.com"),
SUBDOMAIN("test.example.com");
final String domainName;
ValidDomainNameIdentifier(final String domainName) {
this.domainName = domainName;
}
}
@ParameterizedTest
@EnumSource(ValidDomainNameIdentifier.class)
void acceptsValidIdentifier(final ValidDomainNameIdentifier 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).isEmpty();
}
@Test
void containsNoProperties() {
// when
final var validator = HsHostingAssetEntityValidatorRegistry.forType(CLOUD_SERVER);
// then
assertThat(validator.properties()).map(Map::toString).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_SETUP:example.org.bookingItem' must be null but is set to D-???????-?:null",
"'DOMAIN_SETUP:example.org.parentAsset' must be null but is set to D-???????-?:null",
"'DOMAIN_SETUP:example.org.assignedToAsset' must be null but is set to D-???????-?:null");
}
}

View File

@ -33,7 +33,9 @@ class HsHostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.MANAGED_SERVER,
HsHostingAssetType.MANAGED_WEBSPACE,
HsHostingAssetType.UNIX_USER,
HsHostingAssetType.EMAIL_ALIAS
HsHostingAssetType.EMAIL_ALIAS,
HsHostingAssetType.DOMAIN_SETUP,
HsHostingAssetType.DOMAIN_DNS_SETUP
);
}
}

View File

@ -0,0 +1,81 @@
package net.hostsharing.hsadminng.system;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import java.io.IOException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.junit.jupiter.api.condition.OS.LINUX;
class SystemProcessTest {
@Test
@EnabledOnOs(LINUX)
void shouldExecuteAndFetchOutput() throws IOException, InterruptedException {
// given
final var process = new SystemProcess("bash", "-c", "echo 'Hello, World!'; echo 'Error!' >&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);
}
}