Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Hoennig
34c146341e add domain dns setup, WIP 2024-07-03 17:32:05 +02:00
Michael Hoennig
c9e1101035 add domain setup validation 2024-07-03 13:57:39 +02:00
12 changed files with 323 additions and 10 deletions

View File

@ -6,9 +6,10 @@ public enum HsHostingAssetType {
MANAGED_SERVER, // named e.g. vm1234 MANAGED_SERVER, // named e.g. vm1234
MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00 MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00
UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc
DOMAIN_DNS_SETUP(MANAGED_WEBSPACE), // named e.g. example.org DOMAIN_SETUP, // named e.g. example.org
DOMAIN_HTTP_SETUP(MANAGED_WEBSPACE), // named e.g. example.org DOMAIN_DNS_SETUP(DOMAIN_SETUP), // named e.g. example.org
DOMAIN_EMAIL_SETUP(MANAGED_WEBSPACE), // 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 // TODO.spec: SECURE_MX
EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import java.util.regex.Pattern;
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 {
final static String RR_REGEX =
"[a-z0-9\\.-]+\\s+" + // a (e.g. domain) name
"([1-9][0-9]*[HDW]\\s+){0,1}" + // optional TTL
"IN\\s+" + // record class IN for Internet
"([1-9][0-9]*[HDW]\\s+){0,1}" + // optional TTL
"[A-Z]+\\s+([a-z0-9\\.-]+|\"[^\"]*\")\\s*" + // either a simple argument or a more complex in quotes
"(;.*)*"; // optional comment at the end of the line
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+AAAA-RR").withDefault(true),
booleanProperty("auto-MAILSERVICES-RR").withDefault(true), // incl. AUTOCONFIG_RR, AUTODISCOVER_RR
booleanProperty("auto-DKIM-RR").withDefault(true),
booleanProperty("auto-SPF-RR").withDefault(true),
booleanProperty("auto-WILDCARD-MX-RR").withDefault(true),
booleanProperty("auto-WILDCARD-A+AAAA-RR").withDefault(true),
booleanProperty("auto-WILDCARD-DKIM-RR").withDefault(true), // TODO.spec: ask Peter
booleanProperty("auto-WILDCARD-SPF-RR").withDefault(true),
arrayOf(
stringProperty("user-RR").matchesRegEx(RR_REGEX).required()
).optional());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
// FIXME: should be auto-initialized
return Pattern.compile("^" + assetEntity.getParentAsset().getIdentifier() + "$");
}
}

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

@ -20,6 +20,8 @@ public class HsHostingAssetEntityValidatorRegistry {
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator()); register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
register(UNIX_USER, new HsUnixUserHostingAssetValidator()); register(UNIX_USER, new HsUnixUserHostingAssetValidator());
register(EMAIL_ALIAS, new HsEMailAliasHostingAssetValidator()); 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) { private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {

View File

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

View File

@ -9,6 +9,7 @@ create type HsHostingAssetType as enum (
'MANAGED_SERVER', 'MANAGED_SERVER',
'MANAGED_WEBSPACE', 'MANAGED_WEBSPACE',
'UNIX_USER', 'UNIX_USER',
'DOMAIN_SETUP',
'DOMAIN_DNS_SETUP', 'DOMAIN_DNS_SETUP',
'DOMAIN_HTTP_SETUP', 'DOMAIN_HTTP_SETUP',
'DOMAIN_EMAIL_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, alarmContactUuid uuid null references hs_office_contact(uuid) initially deferred,
constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset 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_SERVER' then null
when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER' when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER'
when 'UNIX_USER' then 'MANAGED_WEBSPACE' when 'UNIX_USER' then 'MANAGED_WEBSPACE'
when 'DOMAIN_DNS_SETUP' then 'MANAGED_WEBSPACE' when 'DOMAIN_SETUP' then null
when 'DOMAIN_HTTP_SETUP' then 'MANAGED_WEBSPACE' when 'DOMAIN_DNS_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_EMAIL_SETUP' then 'MANAGED_WEBSPACE' when 'DOMAIN_HTTP_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_EMAIL_SETUP' then 'DOMAIN_SETUP'
when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE' when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE'
when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP' when 'EMAIL_ADDRESS' then 'DOMAIN_EMAIL_SETUP'
when 'PGSQL_USER' then 'MANAGED_WEBSPACE' when 'PGSQL_USER' then 'MANAGED_WEBSPACE'

View File

@ -23,6 +23,7 @@ declare
managedServerUuid uuid; managedServerUuid uuid;
managedWebspaceUuid uuid; managedWebspaceUuid uuid;
webUnixUserUuid uuid; webUnixUserUuid uuid;
domainSetupUuid uuid;
begin begin
currentTask := 'creating hosting-asset test-data ' || givenProjectCaption; currentTask := 'creating hosting-asset test-data ' || givenProjectCaption;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN'); 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 managedServerUuid;
select uuid_generate_v4() into managedWebspaceUuid; select uuid_generate_v4() into managedWebspaceUuid;
select uuid_generate_v4() into webUnixUserUuid; select uuid_generate_v4() into webUnixUserUuid;
select uuid_generate_v4() into domainSetupUuid;
debitorNumberSuffix := relatedDebitor.debitorNumberSuffix; debitorNumberSuffix := relatedDebitor.debitorNumberSuffix;
defaultPrefix := relatedDebitor.defaultPrefix; defaultPrefix := relatedDebitor.defaultPrefix;
@ -75,7 +77,8 @@ begin
(managedWebspaceUuid, relatedManagedWebspaceBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb), (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), (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), (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_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb);
end; $$; end; $$;
--// --//

View File

@ -185,6 +185,24 @@ 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": {}
}
]
"""); """);
final HsHostingAssetType assetType; final HsHostingAssetType assetType;

View File

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

View File

@ -0,0 +1,94 @@
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.CLOUD_SERVER;
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 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(
"www IN CNAME example.com. ; www.example.com is an alias for example.com",
"ns IN A 192.0.2.2 ; IPv4 address for ns.example.com")
)
));
}
@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 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_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");
}
}

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,8 @@ class HsHostingAssetEntityValidatorRegistryUnitTest {
HsHostingAssetType.MANAGED_SERVER, HsHostingAssetType.MANAGED_SERVER,
HsHostingAssetType.MANAGED_WEBSPACE, HsHostingAssetType.MANAGED_WEBSPACE,
HsHostingAssetType.UNIX_USER, HsHostingAssetType.UNIX_USER,
HsHostingAssetType.EMAIL_ALIAS HsHostingAssetType.EMAIL_ALIAS,
HsHostingAssetType.DOMAIN_SETUP
); );
} }
} }