Compare commits
2 Commits
7e004d3eed
...
53259c357b
Author | SHA1 | Date | |
---|---|---|---|
|
53259c357b | ||
|
8ee04ecee3 |
@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration {
|
|||||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||||
return new Jackson2ObjectMapperBuilder()
|
return new Jackson2ObjectMapperBuilder()
|
||||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||||
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
|
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS)
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ public final class HashGenerator {
|
|||||||
"abcdefghijklmnopqrstuvwxyz" +
|
"abcdefghijklmnopqrstuvwxyz" +
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||||
"0123456789/.";
|
"0123456789/.";
|
||||||
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated
|
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
|
||||||
|
|
||||||
public enum Algorithm {
|
public enum Algorithm {
|
||||||
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||||
|
@ -4,13 +4,57 @@ import lombok.Getter;
|
|||||||
import org.springframework.context.ApplicationEvent;
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public class BookingItemCreatedEvent extends ApplicationEvent {
|
public class BookingItemCreatedEvent extends ApplicationEvent {
|
||||||
private final @NotNull HsBookingItem newBookingItem;
|
|
||||||
|
|
||||||
public BookingItemCreatedEvent(@NotNull HsBookingItemController source, @NotNull final HsBookingItem newBookingItem) {
|
private static final Map<UUID, BookingItemCreatedEvent> events = new HashMap<>(); // FIXME: use DB table
|
||||||
|
|
||||||
|
static BookingItemCreatedEvent of(final HsBookingItemRealEntity bookingItem) {
|
||||||
|
return events.get(bookingItem.getUuid());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public static class Status {
|
||||||
|
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
private Status(final String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Status finished() {
|
||||||
|
return new Status(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Status failed(final String errorMessage) {
|
||||||
|
return new Status(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isFinished() {
|
||||||
|
return message == null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final @NotNull UUID bookingItemUuid;
|
||||||
|
private final @NotNull String assetJson;
|
||||||
|
|
||||||
|
private Status status;
|
||||||
|
|
||||||
|
public BookingItemCreatedEvent(
|
||||||
|
@NotNull final HsBookingItemController source,
|
||||||
|
@NotNull final HsBookingItem newBookingItem,
|
||||||
|
final String assetJson) {
|
||||||
super(source);
|
super(source);
|
||||||
this.newBookingItem = newBookingItem;
|
this.bookingItemUuid = newBookingItem.getUuid();
|
||||||
|
this.assetJson = assetJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatus(final Status status) {
|
||||||
|
this.status = status;
|
||||||
|
events.put(bookingItemUuid, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item;
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||||
@ -40,6 +42,9 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsBookingItemRbacRepository bookingItemRepo;
|
private HsBookingItemRbacRepository bookingItemRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper jsonMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private EntityManagerWrapper em;
|
private EntityManagerWrapper em;
|
||||||
|
|
||||||
@ -77,7 +82,12 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||||
.revampProperties();
|
.revampProperties();
|
||||||
|
|
||||||
applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(this, saveProcessor.getEntity()));
|
try {
|
||||||
|
applicationEventPublisher.publishEvent(new BookingItemCreatedEvent(
|
||||||
|
this, saveProcessor.getEntity(), jsonMapper.writeValueAsString(body.getAsset())));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
|
@ -48,7 +48,7 @@ public class BookingItemEntitySaveProcessor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO.impl: remove once the migration of legacy data is done
|
// TODO.legacy: remove once the migration of legacy data is done
|
||||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||||
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||||
step("validateEntity", "prepareForSave");
|
step("validateEntity", "prepareForSave");
|
||||||
|
@ -1,39 +1,30 @@
|
|||||||
|
|
||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
|
||||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS;
|
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRAR_LEVEL_DOMAINS;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||||
|
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||||
|
|
||||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
|
||||||
public static final String TARGET_UNIX_USER_PROPERTY_NAME = "targetUnixUser";
|
|
||||||
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
||||||
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
||||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||||
|
|
||||||
HsDomainSetupBookingItemValidator() {
|
HsDomainSetupBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
|
// TODO.spec: feels wrong
|
||||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||||
.maxLength(253)
|
.maxLength(253)
|
||||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
||||||
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
|
.notMatchesRegEx(REGISTRAR_LEVEL_DOMAINS).describedAs("is a forbidden registrar-level domain name")
|
||||||
.required(),
|
.required(),
|
||||||
// TODO.legacy: remove the following property once we give up legacy compatibility
|
|
||||||
stringProperty(TARGET_UNIX_USER_PROPERTY_NAME).writeOnce()
|
|
||||||
.maxLength(253)
|
|
||||||
.matchesRegEx(TARGET_UNIX_USER_NAME_REGEX).describedAs("is not a valid unix-user name")
|
|
||||||
.writeOnce()
|
|
||||||
.required(),
|
|
||||||
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
stringProperty(VERIFICATION_CODE_PROPERTY_NAME)
|
||||||
.minLength(12)
|
.minLength(12)
|
||||||
.maxLength(64)
|
.maxLength(64)
|
||||||
@ -41,19 +32,6 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<String> validateEntity(final HsBookingItem bookingItem) {
|
|
||||||
final var violations = new ArrayList<String>();
|
|
||||||
final var domainName = bookingItem.getDirectValue(DOMAIN_NAME_PROPERTY_NAME, String.class);
|
|
||||||
if (!bookingItem.isLoaded() &&
|
|
||||||
domainName.matches("hostsharing.(com|net|org|coop|de)")) {
|
|
||||||
violations.add("'" + bookingItem.toShortString() + ".resources." + DOMAIN_NAME_PROPERTY_NAME + "' = '" + domainName
|
|
||||||
+ "' is a forbidden Hostsharing domain name");
|
|
||||||
}
|
|
||||||
violations.addAll(super.validateEntity(bookingItem));
|
|
||||||
return violations;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||||
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
|
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
|
||||||
if (userDefinedVerificationCode != null) {
|
if (userDefinedVerificationCode != null) {
|
||||||
|
@ -1,14 +1,31 @@
|
|||||||
package net.hostsharing.hsadminng.hs.hosting.asset;
|
package net.hostsharing.hsadminng.hs.hosting.asset;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent;
|
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedEvent.Status;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.ApplicationListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import java.util.List;
|
import jakarta.validation.ValidationException;
|
||||||
|
import java.net.IDN;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
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_HTTP_SETUP;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedEvent> {
|
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedEvent> {
|
||||||
@ -16,41 +33,143 @@ public class HsBookingItemCreatedListener implements ApplicationListener<Booking
|
|||||||
@Autowired
|
@Autowired
|
||||||
private EntityManagerWrapper emw;
|
private EntityManagerWrapper emw;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper jsonMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StandardMapper standardMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
public void onApplicationEvent(final BookingItemCreatedEvent event) {
|
public void onApplicationEvent(final BookingItemCreatedEvent event) {
|
||||||
System.out.println("Received newly created booking item: " + event.getNewBookingItem());
|
|
||||||
final var newBookingItemRealEntity =
|
final var newBookingItemRealEntity =
|
||||||
emw.getReference(HsBookingItemRealEntity.class, event.getNewBookingItem().getUuid());
|
emw.getReference(HsBookingItemRealEntity.class, event.getBookingItemUuid());
|
||||||
final var newHostingAsset = switch (newBookingItemRealEntity.getType()) {
|
final var asset = jsonMapper.readValue(event.getAssetJson(), HsHostingAssetAutoInsertResource.class);
|
||||||
|
final var factory = switch (newBookingItemRealEntity.getType()) {
|
||||||
case PRIVATE_CLOUD -> null;
|
case PRIVATE_CLOUD -> null;
|
||||||
case CLOUD_SERVER -> null;
|
case CLOUD_SERVER -> null;
|
||||||
case MANAGED_SERVER -> null;
|
case MANAGED_SERVER -> null;
|
||||||
case MANAGED_WEBSPACE -> null;
|
case MANAGED_WEBSPACE -> null;
|
||||||
case DOMAIN_SETUP -> createDomainSetupHostingAsset(newBookingItemRealEntity);
|
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(newBookingItemRealEntity, asset);
|
||||||
};
|
};
|
||||||
if (newHostingAsset != null) {
|
if (factory != null) {
|
||||||
try {
|
event.setStatus(factory.performSaveProcess());
|
||||||
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
|
||||||
.preprocessEntity()
|
|
||||||
.validateEntity()
|
|
||||||
.prepareForSave()
|
|
||||||
.save()
|
|
||||||
.validateContext();
|
|
||||||
} catch (final Exception e) {
|
|
||||||
// TODO.impl: store status in a separate field, maybe enum+message
|
|
||||||
newBookingItemRealEntity.getResources().put("status", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HsHostingAsset createDomainSetupHostingAsset(final HsBookingItemRealEntity fromBookingItem) {
|
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
|
||||||
return HsHostingAssetRbacEntity.builder()
|
return uuid != null ? emw.getReference(entityClass, uuid) : null;
|
||||||
.bookingItem(fromBookingItem)
|
}
|
||||||
.type(HsHostingAssetType.DOMAIN_SETUP)
|
|
||||||
.identifier(fromBookingItem.getDirectValue("domainName", String.class))
|
@RequiredArgsConstructor
|
||||||
.subHostingAssets(List.of(
|
abstract class HostingAssetFactory {
|
||||||
// TARGET_UNIX_USER_PROPERTY_NAME
|
|
||||||
))
|
final HsBookingItemRealEntity fromBookingItem;
|
||||||
.build();
|
final HsHostingAssetAutoInsertResource asset;
|
||||||
|
|
||||||
|
protected HsHostingAsset create() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status performSaveProcess() {
|
||||||
|
final var newHostingAsset = create();
|
||||||
|
try {
|
||||||
|
return persist(newHostingAsset);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
return Status.failed(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Status persist(final HsHostingAsset newHostingAsset) {
|
||||||
|
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
||||||
|
.preprocessEntity()
|
||||||
|
.validateEntity()
|
||||||
|
.prepareForSave()
|
||||||
|
.save()
|
||||||
|
.validateContext();
|
||||||
|
return Status.finished();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DomainSetupHostingAssetFactory extends HostingAssetFactory {
|
||||||
|
|
||||||
|
public DomainSetupHostingAssetFactory(
|
||||||
|
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||||
|
final HsHostingAssetAutoInsertResource asset) {
|
||||||
|
super(newBookingItemRealEntity, asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HsHostingAsset create() {
|
||||||
|
final String domainName = asset.getIdentifier();
|
||||||
|
final var domainSetupAsset = createDomainSetupAsset(domainName);
|
||||||
|
|
||||||
|
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
|
||||||
|
final var subHostingAssetResources = asset.getSubHostingAssets();
|
||||||
|
final var domainHttpSetupAssetResource = subHostingAssetResources.stream()
|
||||||
|
.filter(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP)
|
||||||
|
.findFirst().orElseThrow(() -> new ValidationException(
|
||||||
|
domainName + ": missing target unix user (assignedToHostingAssetUuid) for DOMAIN_HTTP_SETUP "));
|
||||||
|
final var domainHttpSetupAsset = domainSetupAsset.getSubHostingAssets().stream().filter(sha -> sha.getType() == DOMAIN_HTTP_SETUP).findFirst().orElseThrow();
|
||||||
|
domainHttpSetupAsset.setParentAsset(domainSetupAsset);
|
||||||
|
final HsHostingAssetRealEntity assignedToUnixUserAsset =
|
||||||
|
emw.find(HsHostingAssetRealEntity.class, domainHttpSetupAssetResource.getAssignedToAssetUuid());
|
||||||
|
domainHttpSetupAsset.setAssignedToAsset(assignedToUnixUserAsset);
|
||||||
|
|
||||||
|
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_DNS_SETUP)) {
|
||||||
|
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
|
||||||
|
.type(DOMAIN_DNS_SETUP)
|
||||||
|
.parentAsset(domainSetupAsset)
|
||||||
|
.assignedToAsset(assignedToUnixUserAsset.getParentAsset()) // FIXME: why is that needed?
|
||||||
|
.identifier(domainName + "|DNS")
|
||||||
|
.config(Map.ofEntries(
|
||||||
|
// FIXME:
|
||||||
|
entry("TTL", 21600),
|
||||||
|
entry("auto-SOA", true)
|
||||||
|
))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_MBOX_SETUP)) {
|
||||||
|
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
|
||||||
|
.type(DOMAIN_MBOX_SETUP)
|
||||||
|
.parentAsset(domainSetupAsset)
|
||||||
|
.assignedToAsset(assignedToUnixUserAsset.getParentAsset())
|
||||||
|
.identifier(domainName + "|MBOX")
|
||||||
|
.caption("HTTP-Setup für " + IDN.toUnicode(domainName))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
if (subHostingAssetResources.stream().noneMatch(ha -> ha.getType() == HsHostingAssetTypeResource.DOMAIN_SMTP_SETUP)) {
|
||||||
|
domainSetupAsset.getSubHostingAssets().add(HsHostingAssetRealEntity.builder()
|
||||||
|
.type(DOMAIN_SMTP_SETUP)
|
||||||
|
.parentAsset(domainSetupAsset)
|
||||||
|
.assignedToAsset(assignedToUnixUserAsset.getParentAsset())
|
||||||
|
.identifier(domainName + "|SMTP")
|
||||||
|
.caption("HTTP-Setup für " + IDN.toUnicode(domainName))
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
return domainSetupAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
|
||||||
|
return HsHostingAssetRealEntity.builder()
|
||||||
|
.bookingItem(fromBookingItem)
|
||||||
|
.type(HsHostingAssetType.DOMAIN_SETUP)
|
||||||
|
.identifier(domainName)
|
||||||
|
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
|
||||||
|
.parentAsset(ref(HsHostingAssetRealEntity.class, asset.getParentAssetUuid()))
|
||||||
|
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
|
||||||
|
// FIXME .config(asset.getConfig())
|
||||||
|
.subHostingAssets(
|
||||||
|
standardMapper.mapList(asset.getSubHostingAssets(), HsHostingAssetRealEntity.class)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Status persist(final HsHostingAsset newHostingAsset) {
|
||||||
|
final var status = super.persist(newHostingAsset);
|
||||||
|
newHostingAsset.getSubHostingAssets().forEach(super::persist);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
|
|||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.Repository;
|
import org.springframework.data.repository.Repository;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
|
|||||||
|
|
||||||
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
||||||
|
|
||||||
|
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
|
||||||
|
return findByTypeAndIdentifierImpl(type.name(), identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select ha
|
||||||
|
from HsHostingAssetRealEntity ha
|
||||||
|
where cast(ha.type as String) = :type
|
||||||
|
and ha.identifier = :identifier
|
||||||
|
""")
|
||||||
|
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
select ha.uuid,
|
select ha.uuid,
|
||||||
ha.alarmcontactuuid,
|
ha.alarmcontactuuid,
|
||||||
|
@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO.impl: remove once the migration of legacy data is done
|
// TODO.legacy: remove once the migration of legacy data is done
|
||||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||||
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||||
step("validateEntity", "prepareForSave");
|
step("validateEntity", "prepareForSave");
|
||||||
|
@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
|
|||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
// TODO.impl: make package private once we've migrated the legacy data
|
// TODO.legacy: make package private once we've migrated the legacy data
|
||||||
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||||
|
|
||||||
// according to RFC 1035 (section 5) and RFC 1034
|
// according to RFC 1035 (section 5) and RFC 1034
|
||||||
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
|
|||||||
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||||
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
||||||
|
|
||||||
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
|
private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
|
||||||
|
|
||||||
HsDomainDnsSetupHostingAssetValidator() {
|
HsDomainDnsSetupHostingAssetValidator() {
|
||||||
super(
|
super(
|
||||||
|
@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
// TODO.impl: remove after legacy data is migrated
|
// TODO.legacy: remove after legacy data is migrated
|
||||||
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
|
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
|
||||||
// already hashed => do not validate
|
// already hashed => do not validate
|
||||||
return;
|
return;
|
||||||
|
@ -58,6 +58,17 @@ components:
|
|||||||
nullable: false
|
nullable: false
|
||||||
type:
|
type:
|
||||||
$ref: '#/components/schemas/HsBookingItemType'
|
$ref: '#/components/schemas/HsBookingItemType'
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
description: only used as a default value for automatically created hosting assets, not part of the booking item
|
||||||
|
assignedToHostingAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: false
|
||||||
|
description: only used as a default value for automatically created hosting assets, not part of the booking item
|
||||||
caption:
|
caption:
|
||||||
type: string
|
type: string
|
||||||
minLength: 3
|
minLength: 3
|
||||||
@ -69,6 +80,8 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
resources:
|
resources:
|
||||||
$ref: '#/components/schemas/BookingResources'
|
$ref: '#/components/schemas/BookingResources'
|
||||||
|
asset:
|
||||||
|
$ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert'
|
||||||
required:
|
required:
|
||||||
- caption
|
- caption
|
||||||
- projectUuid
|
- projectUuid
|
||||||
|
@ -94,7 +94,68 @@ components:
|
|||||||
- type
|
- type
|
||||||
- identifier
|
- identifier
|
||||||
- caption
|
- caption
|
||||||
- config
|
additionalProperties: false
|
||||||
|
|
||||||
|
HsHostingAssetAutoInsert:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
parentAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
assignedToAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetType'
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
caption:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
alarmContactUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
config:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||||
|
subHostingAssets:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetSubInsert'
|
||||||
|
required:
|
||||||
|
- identifier
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
|
HsHostingAssetSubInsert:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
assignedToAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetType'
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
caption:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
alarmContactUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
config:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
HsHostingAssetConfiguration:
|
HsHostingAssetConfiguration:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -41,7 +41,7 @@ create table if not exists hs_hosting.asset
|
|||||||
config jsonb not null,
|
config jsonb not null,
|
||||||
alarmContactUuid uuid null references hs_office.contact(uuid) initially deferred,
|
alarmContactUuid uuid null references hs_office.contact(uuid) initially deferred,
|
||||||
|
|
||||||
unique (type, identifier), -- at least as long as we need to be compatible to the legacy system
|
unique (type, identifier), -- TODO.legacy: at least as long as we need to be compatible to the legacy system
|
||||||
|
|
||||||
constraint hosting_asset_has_booking_item_or_parent_asset
|
constraint hosting_asset_has_booking_item_or_parent_asset
|
||||||
check (bookingItemUuid is not null or parentAssetUuid is not null or type in ('DOMAIN_SETUP', 'IPV4_NUMBER', 'IPV6_NUMBER'))
|
check (bookingItemUuid is not null or parentAssetUuid is not null or type in ('DOMAIN_SETUP', 'IPV4_NUMBER', 'IPV6_NUMBER'))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
--liquibase formatted sql
|
--liquibase formatted sql
|
||||||
|
|
||||||
-- TODO: These changesets are just for the external remote views to simulate the legacy tables.
|
-- TODO.legacy: These changesets are just for the external remote views to simulate the legacy tables.
|
||||||
-- Once we don't need the external remote views anymore, create revert changesets.
|
-- Once we don't need the external remote views anymore, create revert changesets.
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
|
@ -5,12 +5,14 @@ import io.restassured.RestAssured;
|
|||||||
import io.restassured.http.ContentType;
|
import io.restassured.http.ContentType;
|
||||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealRepository;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository;
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealRepository;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns;
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns;
|
||||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.junit.jupiter.api.ClassOrderer;
|
import org.junit.jupiter.api.ClassOrderer;
|
||||||
import org.junit.jupiter.api.MethodOrderer;
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
@ -31,6 +33,7 @@ import java.util.UUID;
|
|||||||
import static java.util.Map.entry;
|
import static java.util.Map.entry;
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
|
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
|
||||||
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
import static net.hostsharing.hsadminng.rbac.test.JsonMatcher.lenientlyEquals;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.matchesRegex;
|
import static org.hamcrest.Matchers.matchesRegex;
|
||||||
@ -70,11 +73,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
// given
|
// given
|
||||||
context("superuser-alex@hostsharing.net");
|
context("superuser-alex@hostsharing.net");
|
||||||
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
|
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||||
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@ -138,11 +137,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
void globalAdmin_canAddBookingItem() {
|
void globalAdmin_canAddBookingItem() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
|
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||||
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
|
|
||||||
.flatMap(List::stream)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
final var location = RestAssured // @formatter:off
|
final var location = RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@ -189,34 +184,41 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
void projectAgent_canAddBookingItemWithHostingAsset() {
|
void projectAgent_canAddBookingItemWithHostingAsset() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
|
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
|
||||||
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
|
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||||
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
|
final var givenUnixUser = realHostingAssetRepo.findByTypeAndIdentifier(UNIX_USER, "fir01-web").stream()
|
||||||
.flatMap(List::stream)
|
.findFirst().orElseThrow();
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
Dns.fakeResultForDomain("example.org",
|
Dns.fakeResultForDomain("example.org",
|
||||||
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
Dns.Result.fromRecords("Hostsharing-domain-setup-verification-code=just-a-fake-verification-code"));
|
||||||
|
|
||||||
final var location = RestAssured // @formatter:off
|
final var location = RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("current-subject", "superuser-alex@hostsharing.net")
|
.header("current-subject", "superuser-alex@hostsharing.net")
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"projectUuid": "{projectUuid}",
|
"projectUuid": "{projectUuid}",
|
||||||
"type": "DOMAIN_SETUP",
|
"type": "DOMAIN_SETUP",
|
||||||
"caption": "some new domain-setup booking",
|
"caption": "Domain-Setup for example.org",
|
||||||
"resources": {
|
"resources": {
|
||||||
"domainName": "example.org",
|
"domainName": "example.org",
|
||||||
"targetUnixUser": "fir01-web",
|
"verificationCode": "just-a-fake-verification-code"
|
||||||
"verificationCode": "just-a-fake-verification-code"
|
},
|
||||||
|
"asset": { // FIXME: rename to hostingAsset
|
||||||
|
"identifier": "example.org", // also as default for all subAssets
|
||||||
|
"subHostingAssets": [
|
||||||
|
{
|
||||||
|
"type": "DOMAIN_HTTP_SETUP",
|
||||||
|
"assignedToAssetUuid": "{unixUserUuid}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
"""
|
||||||
"""
|
.replace("{projectUuid}", givenProject.getUuid().toString())
|
||||||
.replace("{projectUuid}", givenProject.getUuid().toString())
|
.replace("{unixUserUuid}", givenUnixUser.getUuid().toString())
|
||||||
)
|
)
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.post("http://localhost/api/hs/booking/items")
|
.post("http://localhost/api/hs/booking/items")
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
@ -225,10 +227,9 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
.body("", lenientlyEquals("""
|
.body("", lenientlyEquals("""
|
||||||
{
|
{
|
||||||
"type": "DOMAIN_SETUP",
|
"type": "DOMAIN_SETUP",
|
||||||
"caption": "some new domain-setup booking",
|
"caption": "Domain-Setup for example.org",
|
||||||
"validFrom": "{today}",
|
"validFrom": "{today}",
|
||||||
"validTo": null,
|
"validTo": null
|
||||||
"resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" }
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
.replace("{today}", LocalDate.now().toString())
|
.replace("{today}", LocalDate.now().toString())
|
||||||
@ -240,24 +241,37 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
// then, the new BookingItem can be accessed under the generated UUID
|
// then, the new BookingItem can be accessed under the generated UUID
|
||||||
final var newBookingItem = fetchRealBookingItemFromURI(location);
|
final var newBookingItem = fetchRealBookingItemFromURI(location);
|
||||||
assertThat(newBookingItem)
|
assertThat(newBookingItem)
|
||||||
.extracting(bi -> bi.getDirectValue("domainName", String.class))
|
.extracting(HsBookingItem::getCaption)
|
||||||
.isEqualTo("example.org");
|
.isEqualTo("Domain-Setup for example.org");
|
||||||
|
|
||||||
// and the related HostingAsset also got created
|
// and the related HostingAssets are also got created
|
||||||
assertThat(realHostingAssetRepo.findByIdentifier("example.org")).isNotEmpty()
|
final var domainSetupHostingAsset = realHostingAssetRepo.findByIdentifier("example.org");
|
||||||
|
assertThat(domainSetupHostingAsset).isNotEmpty()
|
||||||
.map(HsHostingAsset::getBookingItem)
|
.map(HsHostingAsset::getBookingItem)
|
||||||
.contains(newBookingItem);
|
.contains(newBookingItem);
|
||||||
|
assertThat(realHostingAssetRepo.findByIdentifier("example.org|DNS")).isNotEmpty()
|
||||||
|
.map(HsHostingAsset::getParentAsset)
|
||||||
|
.isEqualTo(domainSetupHostingAsset);
|
||||||
|
assertThat(realHostingAssetRepo.findByIdentifier("example.org|HTTP")).isNotEmpty()
|
||||||
|
.map(HsHostingAsset::getParentAsset)
|
||||||
|
.isEqualTo(domainSetupHostingAsset);
|
||||||
|
assertThat(realHostingAssetRepo.findByIdentifier("example.org|MBOX")).isNotEmpty()
|
||||||
|
.map(HsHostingAsset::getParentAsset)
|
||||||
|
.isEqualTo(domainSetupHostingAsset);
|
||||||
|
assertThat(realHostingAssetRepo.findByIdentifier("example.org|SMTP")).isNotEmpty()
|
||||||
|
.map(HsHostingAsset::getParentAsset)
|
||||||
|
.isEqualTo(domainSetupHostingAsset);
|
||||||
|
final var status = BookingItemCreatedEvent.of(newBookingItem);
|
||||||
|
assertThat(status.getStatus().isFinished());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void projectAgent_canAddBookingItemEvenIfHostingAssetCreationFails() {
|
void projectAgent_canAddBookingItemEvenIfHostingAssetCreationFails() {
|
||||||
|
|
||||||
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
|
context.define("superuser-alex@hostsharing.net", "hs_booking.project#D-1000111-D-1000111defaultproject:AGENT");
|
||||||
final var givenProject = debitorRepo.findByDebitorNumber(1000111).stream()
|
final var givenProject = findDefaultProjectOfDebitorNumber(1000111);
|
||||||
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
|
final var givenUnixUser = realHostingAssetRepo.findByIdentifier("fir01-web").stream().findFirst().orElseThrow();
|
||||||
.flatMap(List::stream)
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow();
|
|
||||||
|
|
||||||
Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode
|
Dns.fakeResultForDomain("example.org", Dns.Result.fromRecords()); // without valid verificationCode
|
||||||
|
|
||||||
@ -272,12 +286,21 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"caption": "some new domain-setup booking",
|
"caption": "some new domain-setup booking",
|
||||||
"resources": {
|
"resources": {
|
||||||
"domainName": "example.org",
|
"domainName": "example.org",
|
||||||
"targetUnixUser": "fir01-web",
|
|
||||||
"verificationCode": "just-a-fake-verification-code"
|
"verificationCode": "just-a-fake-verification-code"
|
||||||
|
},
|
||||||
|
"asset": { // FIXME: rename to hostingAsset
|
||||||
|
"identifier": "example.org", // also as default for all subAssets
|
||||||
|
"subHostingAssets": [
|
||||||
|
{
|
||||||
|
"type": "DOMAIN_HTTP_SETUP",
|
||||||
|
"assignedToAssetUuid": "{unixUserUuid}"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
.replace("{projectUuid}", givenProject.getUuid().toString())
|
.replace("{projectUuid}", givenProject.getUuid().toString())
|
||||||
|
.replace("{unixUserUuid}", givenUnixUser.getUuid().toString())
|
||||||
)
|
)
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
@ -291,7 +314,7 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"caption": "some new domain-setup booking",
|
"caption": "some new domain-setup booking",
|
||||||
"validFrom": "{today}",
|
"validFrom": "{today}",
|
||||||
"validTo": null,
|
"validTo": null,
|
||||||
"resources": { "domainName": "example.org", "targetUnixUser": "fir01-web" }
|
"resources": { "domainName": "example.org" }
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
.replace("{today}", LocalDate.now().toString())
|
.replace("{today}", LocalDate.now().toString())
|
||||||
@ -305,8 +328,8 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
assertThat(newBookingItem)
|
assertThat(newBookingItem)
|
||||||
.extracting(bi -> bi.getDirectValue("domainName", String.class))
|
.extracting(bi -> bi.getDirectValue("domainName", String.class))
|
||||||
.isEqualTo("example.org");
|
.isEqualTo("example.org");
|
||||||
assertThat(newBookingItem)
|
final var status = BookingItemCreatedEvent.of(newBookingItem);
|
||||||
.extracting(bi -> bi.getDirectValue("status", String.class))
|
assertThat(status.getStatus().getMessage())
|
||||||
.isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]");
|
.isEqualTo("[[DNS] no TXT record 'Hostsharing-domain-setup-verification-code=just-a-fake-verification-code' found for domain name 'example.org' (nor in its super-domain)]");
|
||||||
|
|
||||||
// but the related HostingAsset did not get created
|
// but the related HostingAsset did not get created
|
||||||
@ -314,6 +337,14 @@ class HsBookingItemControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @NotNull HsBookingProjectRealEntity findDefaultProjectOfDebitorNumber(final int debitorNumber) {
|
||||||
|
return debitorRepo.findByDebitorNumber(debitorNumber).stream()
|
||||||
|
.map(d -> realProjectRepo.findAllByDebitorUuid(d.getUuid()))
|
||||||
|
.flatMap(List::stream)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@Order(1)
|
@Order(1)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
@ -36,8 +36,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.project(project)
|
.project(project)
|
||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", "example.org"),
|
entry("domainName", "example.org")
|
||||||
entry("targetUnixUser", "xyz00")
|
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -59,7 +58,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", "example.org"),
|
entry("domainName", "example.org"),
|
||||||
entry("targetUnixUser", "xyz00"),
|
|
||||||
entry("verificationCode", "1234-5678-9100")
|
entry("verificationCode", "1234-5678-9100")
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
@ -80,8 +78,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.project(project)
|
.project(project)
|
||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253)),
|
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 253))
|
||||||
entry("targetUnixUser", "xyz00")
|
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -99,8 +96,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.project(project)
|
.project(project)
|
||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254)),
|
entry("domainName", right(TOO_LONG_DOMAIN_NAME, 254))
|
||||||
entry("targetUnixUser", "xyz00")
|
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -118,8 +114,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.project(project)
|
.project(project)
|
||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", "example.com"),
|
entry("domainName", "example.com")
|
||||||
entry("targetUnixUser", "xyz00-test")
|
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -130,25 +125,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
assertThat(result).isEmpty();
|
assertThat(result).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void rejectsInvalidUnixUser() {
|
|
||||||
final var domainSetupBookingItemEntity = HsBookingItemRealEntity.builder()
|
|
||||||
.type(DOMAIN_SETUP)
|
|
||||||
.project(project)
|
|
||||||
.caption("Test-Domain")
|
|
||||||
.resources(Map.ofEntries(
|
|
||||||
entry("domainName", "example.com"),
|
|
||||||
entry("targetUnixUser", "xyz00test")
|
|
||||||
))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// when
|
|
||||||
final var result = HsBookingItemEntityValidatorRegistry.doValidate(em, domainSetupBookingItemEntity);
|
|
||||||
|
|
||||||
// then
|
|
||||||
assertThat(result).contains("'D-12345:Test-Project:Test-Domain.resources.targetUnixUser' = 'xyz00test' is not a valid unix-user name");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@ValueSource(strings = {
|
@ValueSource(strings = {
|
||||||
"de", "com", "net", "org", "actually-any-top-level-domain",
|
"de", "com", "net", "org", "actually-any-top-level-domain",
|
||||||
@ -196,8 +172,7 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
.project(project)
|
.project(project)
|
||||||
.caption("Test-Domain")
|
.caption("Test-Domain")
|
||||||
.resources(Map.ofEntries(
|
.resources(Map.ofEntries(
|
||||||
entry("domainName", secondLevelRegistrarDomain),
|
entry("domainName", secondLevelRegistrarDomain)
|
||||||
entry("targetUnixUser", "xyz00")
|
|
||||||
))
|
))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
@ -219,7 +194,6 @@ class HsDomainSetupBookingItemValidatorUnitTest {
|
|||||||
// then
|
// then
|
||||||
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
assertThat(validator.properties()).map(Map::toString).containsExactlyInAnyOrder(
|
||||||
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, maxLength=253, required=true, writeOnce=true}",
|
"{type=string, propertyName=domainName, matchesRegEx=[^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}], matchesRegExDescription=is not a (non-top-level) fully qualified domain name, notMatchesRegEx=[[^.]+, (co|org|gov|ac|sch)\\.uk, (com|net|org|edu|gov|asn|id)\\.au, (co|ne|or|ac|go)\\.jp, (com|net|org|gov|edu|ac)\\.cn, (com|net|org|gov|edu|mil|art)\\.br, (co|net|org|gen|firm|ind)\\.in, (com|net|org|gob|edu)\\.mx, (gov|edu)\\.it, (co|net|org|govt|ac|school|geek|kiwi)\\.nz, (co|ne|or|go|re|pe)\\.kr], notMatchesRegExDescription=is a forbidden registrar-level domain name, maxLength=253, required=true, writeOnce=true}",
|
||||||
"{type=string, propertyName=targetUnixUser, matchesRegEx=[^[a-z][a-z0-9]{2}[0-9]{2}$|^[a-z][a-z0-9]{2}[0-9]{2}-[a-z0-9\\._-]+$], matchesRegExDescription=is not a valid unix-user name, maxLength=253, required=true, writeOnce=true}",
|
|
||||||
"{type=string, propertyName=verificationCode, minLength=12, maxLength=64, computed=IN_INIT}");
|
"{type=string, propertyName=verificationCode, minLength=12, maxLength=64, computed=IN_INIT}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -502,7 +502,7 @@ public abstract class BaseOfficeDataImport extends CsvDataImport {
|
|||||||
// this happens if a natural person is marked as 'contractual' for itself
|
// this happens if a natural person is marked as 'contractual' for itself
|
||||||
final var idsToRemove = new HashSet<Integer>();
|
final var idsToRemove = new HashSet<Integer>();
|
||||||
relations.forEach((id, r) -> {
|
relations.forEach((id, r) -> {
|
||||||
if (r.getHolder() == r.getAnchor()) {
|
if (r.getType() == HsOfficeRelationType.REPRESENTATIVE && r.getHolder() == r.getAnchor()) {
|
||||||
idsToRemove.add(id);
|
idsToRemove.add(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user