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:
parent
a77eaefb94
commit
f6d66d5712
@ -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)
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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") );
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ components:
|
||||
- MANAGED_SERVER
|
||||
- MANAGED_WEBSPACE
|
||||
- UNIX_USER
|
||||
- DOMAIN_SETUP
|
||||
- DOMAIN_DNS_SETUP
|
||||
- DOMAIN_HTTP_SETUP
|
||||
- DOMAIN_EMAIL_SETUP
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
@ -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; $$;
|
||||
--//
|
||||
|
||||
|
@ -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;
|
||||
|
@ -35,7 +35,9 @@ class HsHostingAssetPropsControllerAcceptanceTest {
|
||||
"MANAGED_WEBSPACE",
|
||||
"CLOUD_SERVER",
|
||||
"UNIX_USER",
|
||||
"EMAIL_ALIAS"
|
||||
"EMAIL_ALIAS",
|
||||
"DOMAIN_SETUP",
|
||||
"DOMAIN_DNS_SETUP"
|
||||
]
|
||||
"""));
|
||||
// @formatter:on
|
||||
|
@ -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());
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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."
|
||||
);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user