add-domain-setup-validation (#71)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/71 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@ -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();
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user