1
0

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