Merge branch 'master' into TP-202405-filtered_import

This commit is contained in:
Dev und Test fuer hsadminng 2024-07-12 07:00:43 +02:00
commit 2331d66887
160 changed files with 9967 additions and 1784 deletions

View File

@ -66,6 +66,7 @@ dependencies {
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
implementation 'org.apache.commons:commons-text:1.11.0'
implementation 'net.java.dev.jna:jna:5.8.0'
implementation 'org.modelmapper:modelmapper:3.2.0'
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'

View File

@ -0,0 +1,199 @@
## HostingAsset Type Structure
### Domain
```plantuml
@startuml
left to right direction
package Booking #feb28c {
entity BI_PRIVATE_CLOUD
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
package Domain #99bcdb {
entity HA_DOMAIN_SETUP
entity HA_DOMAIN_DNS_SETUP
entity HA_DOMAIN_HTTP_SETUP
entity HA_DOMAIN_SMTP_SETUP
entity HA_DOMAIN_MBOX_SETUP
entity HA_EMAIL_ADDRESS
}
package Server #99bcdb {
entity HA_CLOUD_SERVER
entity HA_MANAGED_SERVER
entity HA_IP_NUMBER
}
package Webspace #99bcdb {
entity HA_MANAGED_WEBSPACE
entity HA_UNIX_USER
entity HA_EMAIL_ALIAS
}
}
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_DOMAIN_SETUP o..> HA_DOMAIN_SETUP
HA_DOMAIN_DNS_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_HTTP_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_HTTP_SETUP o..> HA_UNIX_USER
HA_DOMAIN_SMTP_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_SMTP_SETUP o..> HA_MANAGED_WEBSPACE
HA_DOMAIN_MBOX_SETUP *==> HA_DOMAIN_SETUP
HA_DOMAIN_MBOX_SETUP o..> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ADDRESS *==> HA_DOMAIN_MBOX_SETUP
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER
HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE
package Legend #white {
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
}
Booking -down[hidden]->Legend
```
### MariaDB
```plantuml
@startuml
left to right direction
package Booking #feb28c {
entity BI_PRIVATE_CLOUD
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
package MariaDB #99bcdb {
entity HA_MARIADB_INSTANCE
entity HA_MARIADB_USER
entity HA_MARIADB_DATABASE
}
package Server #99bcdb {
entity HA_CLOUD_SERVER
entity HA_MANAGED_SERVER
entity HA_IP_NUMBER
}
package Webspace #99bcdb {
entity HA_MANAGED_WEBSPACE
entity HA_UNIX_USER
entity HA_EMAIL_ALIAS
}
}
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_MARIADB_INSTANCE *==> HA_MANAGED_SERVER
HA_MARIADB_USER *==> HA_MARIADB_INSTANCE
HA_MARIADB_USER o..> HA_MANAGED_WEBSPACE
HA_MARIADB_DATABASE *==> HA_MANAGED_WEBSPACE
HA_MARIADB_DATABASE o..> HA_MARIADB_INSTANCE
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER
HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE
package Legend #white {
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
}
Booking -down[hidden]->Legend
```
### PostgreSQL
```plantuml
@startuml
left to right direction
package Booking #feb28c {
entity BI_PRIVATE_CLOUD
entity BI_CLOUD_SERVER
entity BI_MANAGED_SERVER
entity BI_MANAGED_WEBSPACE
entity BI_DOMAIN_DNS_SETUP
entity BI_DOMAIN_SMTP_SETUP
}
package Hosting #feb28c{
package PostgreSQL #99bcdb {
entity HA_PGSQL_INSTANCE
entity HA_PGSQL_USER
entity HA_PGSQL_DATABASE
}
package Server #99bcdb {
entity HA_CLOUD_SERVER
entity HA_MANAGED_SERVER
entity HA_IP_NUMBER
}
package Webspace #99bcdb {
entity HA_MANAGED_WEBSPACE
entity HA_UNIX_USER
entity HA_EMAIL_ALIAS
}
}
BI_CLOUD_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_SERVER *--> BI_PRIVATE_CLOUD
BI_MANAGED_WEBSPACE *--> BI_MANAGED_SERVER
HA_CLOUD_SERVER ==* BI_CLOUD_SERVER
HA_MANAGED_SERVER ==* BI_MANAGED_SERVER
HA_MANAGED_WEBSPACE ==* BI_MANAGED_WEBSPACE
HA_MANAGED_WEBSPACE o..> HA_MANAGED_SERVER
HA_UNIX_USER *==> HA_MANAGED_WEBSPACE
HA_EMAIL_ALIAS *==> HA_MANAGED_WEBSPACE
HA_PGSQL_INSTANCE *==> HA_MANAGED_SERVER
HA_PGSQL_USER *==> HA_PGSQL_INSTANCE
HA_PGSQL_USER o..> HA_MANAGED_WEBSPACE
HA_PGSQL_DATABASE *==> HA_MANAGED_WEBSPACE
HA_PGSQL_DATABASE o..> HA_PGSQL_INSTANCE
HA_IP_NUMBER o..> HA_CLOUD_SERVER
HA_IP_NUMBER o..> HA_MANAGED_SERVER
HA_IP_NUMBER o..> HA_MANAGED_WEBSPACE
package Legend #white {
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
}
Booking -down[hidden]->Legend
```
This code generated was by HsHostingAssetType.main, do not amend manually.

View File

@ -0,0 +1,288 @@
## HSAdmin-NG
### Project/BookingItems/HostingEntities
__ATTENTION__: The notation uses UML clas diagram elements, but partly with different meanings. See Agenda.
```mermaid
classDiagram
direction TD
Partner o-- "0..n" Membership
Partner *-- "1..n" Debitor
Debitor *-- "1..n" Project
Project o-- "0..n" PrivateCloudBI
Project o-- "0..n" CloudServerBI
Project o-- "0..n" ManagedServerBI
Project o-- "0..n" ManagedWebspaceBI
PrivateCloudBI o-- "0..n" ManagedServerBI
PrivateCloudBI o-- "0..n" CloudServerBI
CloudServerBI *-- CloudServerHE
ManagedServerBI *-- ManagedServerHE
ManagedServerBI o-- "0..n" ManagedWebspaceBI
ManagedWebspaceBI *-- ManagedWebspaceHE
ManagedWebspaceHE *-- "1..n" UnixUserHE
ManagedWebspaceHE o-- "0..n" DomainDNSSetupHE
ManagedWebspaceHE o-- "0..n" DomainHttpSetupHE
ManagedWebspaceHE o-- "0..n" DomainEMailSetupHE
ManagedWebspaceHE o-- "0..n" EMailAliasHE
DomainEMailSetupHE o-- "0..n" EMailAddressHE
ManagedWebspaceHE o-- "0..n" MariaDBUserHE
MariaDBUserHE o-- "0..n" MariaDBHE
ManagedWebspaceHE o-- "0..n" PostgresDBUserHE
PostgresDBUserHE o-- "0..n" PostgresDBHE
DomainHttpSetupHE --|> UnixUserHE : assignedToAsset
ManagedWebspaceHE --|> ManagedServerHE
namespace Office {
class Partner {
}
class Membership {
}
class Debitor {
}
}
namespace Booking {
class Project {
+caption
+create()
}
class PrivateCloudBI {
+caption
~resources = [
+CPUs
+RAM
+SSD
+HDD
+Traffic
]
+book()
}
class CloudServerBI {
+caption
~resources = [
+CPUs
+RAM
+SSD
+HDD
+Traffic
]
+book()
}
class ManagedServerBI {
+caption
~respources = [
+CPUs
+RAM
+SSD
+HDD
+Traffic
]
+book()
}
class ManagedWebspaceBI {
+caption
~resources = [
+SSD
+HDD
+Traffic
+MultiOptions
+Daemons
]
+book()
}
}
style Project stroke:blue,stroke-width:4px
style PrivateCloudBI stroke:blue,stroke-width:4px
style CloudServerBI stroke:blue,stroke-width:4px
style ManagedServerBI stroke:blue,stroke-width:4px
style ManagedWebspaceBI stroke:blue,stroke-width:4px
%% ---------------------------------------------------------
namespace HostingServers {
%% separate (pseudo-) namespace just for better rendering
class CloudServerHE {
-identifier, e.g. "vm1234"
-caption := bi.caption?
-parentAsset := parentHost
-identifier := serverName
-create()
}
class ManagedServerHE {
-identifier, e.g. "vm1234"
-caption := bi.caption?
-parentAsset := parentHost
-identifier := serverName
~config = [
+installed Software
]
-create()
}
}
namespace Hosting {
class ManagedWebspaceHE {
-parentAsset := parentManagedServer
-identifier : webspaceName
+caption
-create()
}
class UnixUserHE {
+identifier ["xyz00-..."]
+caption
~config = [
+SSD Soft Quota
+SSD Hard Quota
+HDD Soft Quota
+HDD Hard Quota
#shell
#password
]
+create()
}
class DomainDNSSetupHE {
+identifier, e.g. "example.com"
+caption
+create()
}
class DomainHttpSetupHE {
+identifier, e.g. "example.com"
+caption
+create()
}
class DomainEMailSetupHE {
+identifier, e.g. "example.com"
+caption
+create()
}
class EMailAliasHE {
+identifier, e.g "xyz00-..."
+caption
~config = [
+target[]
]
+create()
}
class EMailAddressHE {
+identifier, e.g. "test@example.org"
+caption
~config = [
+sub-domain
+local-part
+target
]
+create()
}
class MariaDBUserHE {
+identifier, e.g. "xyz00_mydb"
+caption
config = [
#password
]
+create()
}
class MariaDBHE {
+identifier, e.g. "xyz00_mydb"
+caption
~config = [
+encoding
]
+create()
}
class PostgresDBUserHE {
+identifier, e.g. "xyz00_mydb"
+caption
~config = [
#password
]
+create()
}
class PostgresDBHE {
+identifier, e.g. "xyz00_mydb"
+caption
~config = [
+encoding
+extensions
]
+create()
}
}
style CloudServerHE stroke:orange,stroke-width:4px
style ManagedServerHE stroke:orange,stroke-width:4px
style ManagedWebspaceHE stroke:orange,stroke-width:4px
style UnixUserHE stroke:blue,stroke-width:4px
style DomainDNSSetupHE stroke:blue,stroke-width:4px
style DomainHttpSetupHE stroke:blue,stroke-width:4px
style DomainEMailSetupHE stroke:blue,stroke-width:4px
style EMailAliasHE stroke:blue,stroke-width:4px
style EMailAddressHE stroke:blue,stroke-width:4px
style MariaDBUserHE stroke:blue,stroke-width:4px
style MariaDBHE stroke:blue,stroke-width:4px
style PostgresDBUserHE stroke:blue,stroke-width:4px
style PostgresDBHE stroke:blue,stroke-width:4px
%% --------------------------------------
ParentA o-- ChildA : can contain
ParentB *-- ChildB : contains
namespace Agenda {
class ParentA {
}
class ChildA {
}
class ParentB {
}
class ChildB {
}
class CreatedByClient {
}
class CreatedAutomatically {
}
class SomeEntity {
~patchable = [
%% the following indentations uses two U+2800 to have effect in the rendered diagram
+first
+second
]
-readOnly for client accounts
+readWrite for client accounts
#writeOnly
}
}
style CreatedByClient stroke:blue,stroke-width:4px
style CreatedAutomatically stroke:orange,stroke-width:4px
end
```

View File

@ -9,7 +9,7 @@ import org.springframework.web.context.request.WebRequest;
import java.time.LocalDateTime;
@Getter
class CustomErrorResponse {
public class CustomErrorResponse {
static ResponseEntity<CustomErrorResponse> errorResponse(
final WebRequest request,

View File

@ -0,0 +1,23 @@
package net.hostsharing.hsadminng.errors;
import jakarta.validation.ValidationException;
import java.util.List;
import static java.lang.String.join;
public class MultiValidationException extends ValidationException {
private MultiValidationException(final List<String> violations) {
super(
violations.size() > 1
? "[\n" + join(",\n", violations) + "\n]"
: "[" + join(",\n", violations) + "]"
);
}
public static void throwIfNotEmpty(final List<String> violations) {
if (!violations.isEmpty()) {
throw new MultiValidationException(violations);
}
}
}

View File

@ -73,9 +73,10 @@ public class RestResponseEntityExceptionHandler
}
@ExceptionHandler({ Iban4jException.class, ValidationException.class })
protected ResponseEntity<CustomErrorResponse> handleIbanAndBicExceptions(
protected ResponseEntity<CustomErrorResponse> handleValidationExceptions(
final Throwable exc, final WebRequest request) {
final var message = line(NestedExceptionUtils.getMostSpecificCause(exc).getMessage(), 0);
final String fullMessage = NestedExceptionUtils.getMostSpecificCause(exc).getMessage();
final var message = exc instanceof MultiValidationException ? fullMessage : line(fullMessage, 0);
return errorResponse(request, HttpStatus.BAD_REQUEST, message);
}

View File

@ -0,0 +1,112 @@
package net.hostsharing.hsadminng.hash;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.random.RandomGenerator;
import com.sun.jna.Library;
import com.sun.jna.Native;
public class LinuxEtcShadowHashGenerator {
private static final RandomGenerator random = new SecureRandom();
private static final Queue<String> predefinedSalts = new PriorityQueue<>();
public static final int SALT_LENGTH = 16;
private final String plaintextPassword;
private Algorithm algorithm;
public enum Algorithm {
SHA512("6"),
YESCRYPT("y");
final String prefix;
Algorithm(final String prefix) {
this.prefix = prefix;
}
static Algorithm byPrefix(final String prefix) {
return Arrays.stream(Algorithm.values()).filter(a -> a.prefix.equals(prefix)).findAny()
.orElseThrow(() -> new IllegalArgumentException("unknown hash algorithm: '" + prefix + "'"));
}
}
private static final String SALT_CHARACTERS =
"abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"0123456789/.";
private String salt;
public static LinuxEtcShadowHashGenerator hash(final String plaintextPassword) {
return new LinuxEtcShadowHashGenerator(plaintextPassword);
}
private LinuxEtcShadowHashGenerator(final String plaintextPassword) {
this.plaintextPassword = plaintextPassword;
}
public LinuxEtcShadowHashGenerator using(final Algorithm algorithm) {
this.algorithm = algorithm;
return this;
}
void verify(final String givenHash) {
final var parts = givenHash.split("\\$");
if (parts.length < 3 || parts.length > 5) {
throw new IllegalArgumentException("not a " + algorithm.name() + " Linux hash: " + givenHash);
}
algorithm = Algorithm.byPrefix(parts[1]);
salt = parts.length == 4 ? parts[2] : parts[2] + "$" + parts[3];
if (!generate().equals(givenHash)) {
throw new IllegalArgumentException("invalid password");
}
}
public String generate() {
if (salt == null) {
throw new IllegalStateException("no salt given");
}
if (plaintextPassword == null) {
throw new IllegalStateException("no password given");
}
return NativeCryptLibrary.INSTANCE.crypt(plaintextPassword, "$" + algorithm.prefix + "$" + salt);
}
public static void nextSalt(final String salt) {
predefinedSalts.add(salt);
}
public LinuxEtcShadowHashGenerator withSalt(final String salt) {
this.salt = salt;
return this;
}
public LinuxEtcShadowHashGenerator withRandomSalt() {
if (!predefinedSalts.isEmpty()) {
return withSalt(predefinedSalts.poll());
}
final var stringBuilder = new StringBuilder(SALT_LENGTH);
for (int i = 0; i < SALT_LENGTH; ++i) {
int randomIndex = random.nextInt(SALT_CHARACTERS.length());
stringBuilder.append(SALT_CHARACTERS.charAt(randomIndex));
}
return withSalt(stringBuilder.toString());
}
public static void main(String[] args) {
System.out.println(NativeCryptLibrary.INSTANCE.crypt("given password", "$6$abcdefghijklmno"));
}
public interface NativeCryptLibrary extends Library {
NativeCryptLibrary INSTANCE = Native.load("crypt", NativeCryptLibrary.class);
String crypt(String password, String salt);
}
}

View File

@ -0,0 +1,55 @@
package net.hostsharing.hsadminng.hs.booking.debitor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
@Entity
@Table(name = "hs_booking_debitor_rv")
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@DisplayName("BookingDebitor")
public class HsBookingDebitorEntity implements Stringifyable {
public static final String DEBITOR_NUMBER_TAG = "D-";
private static Stringify<HsBookingDebitorEntity> stringify =
stringify(HsBookingDebitorEntity.class, "booking-debitor")
.withIdProp(HsBookingDebitorEntity::toShortString)
.withProp(HsBookingDebitorEntity::getDefaultPrefix)
.quotedValues(false);
@Id
private UUID uuid;
@Column(name = "debitornumber")
private Integer debitorNumber;
@Column(name = "defaultprefix", columnDefinition = "char(3) not null")
private String defaultPrefix;
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return DEBITOR_NUMBER_TAG + debitorNumber;
}
}

View File

@ -0,0 +1,14 @@
package net.hostsharing.hsadminng.hs.booking.debitor;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsBookingDebitorRepository extends Repository<HsBookingDebitorEntity, UUID> {
Optional<HsBookingDebitorEntity> findByUuid(UUID id);
List<HsBookingDebitorEntity> findByDebitorNumber(int debitorNumber);
}

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsA
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
@ -13,11 +14,13 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.time.LocalDate;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidators.valid;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
@RestController
@ -32,15 +35,18 @@ public class HsBookingItemController implements HsBookingItemsApi {
@Autowired
private HsBookingItemRepository bookingItemRepo;
@PersistenceContext
private EntityManager em;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByDebitorUuid(
public ResponseEntity<List<HsBookingItemResource>> listBookingItemsByProjectUuid(
final String currentUser,
final String assumedRoles,
final UUID debitorUuid) {
final UUID projectUuid) {
context.define(currentUser, assumedRoles);
final var entities = bookingItemRepo.findAllByDebitorUuid(debitorUuid);
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
@ -57,7 +63,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
final var entityToSave = mapper.map(body, HsBookingItemEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = bookingItemRepo.save(valid(entityToSave));
final var saved = HsBookingItemEntityValidatorRegistry.validated(bookingItemRepo.save(entityToSave));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
@ -78,6 +84,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
context.define(currentUser, assumedRoles);
final var result = bookingItemRepo.findByUuid(bookingItemUuid);
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
return result
.map(bookingItemEntity -> ResponseEntity.ok(
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
@ -112,7 +119,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
new HsBookingItemEntityPatcher(current).apply(body);
final var saved = bookingItemRepo.save(valid(current));
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(current));
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(mapped);
}
@ -124,9 +131,8 @@ public class HsBookingItemController implements HsBookingItemsApi {
}
};
@SuppressWarnings("unchecked")
final BiConsumer<HsBookingItemInsertResource, HsBookingItemEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.setValidity(toPostgresDateRange(resource.getValidFrom(), resource.getValidTo()));
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
entity.putResources(KeyValueMap.from(resource.getResources()));
};
}

View File

@ -9,9 +9,9 @@ import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -20,32 +20,37 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.io.IOException;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static java.util.Collections.emptyMap;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.lowerInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.upperInclusiveFromPostgresDateRange;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
@ -55,21 +60,20 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Builder
@Entity
@Builder(toBuilder = true)
@Table(name = "hs_booking_item_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatable<HsBookingItemEntity, HsBookingItemType> {
public class HsBookingItemEntity implements Stringifyable, RbacObject, PropertiesProvider {
private static Stringify<HsBookingItemEntity> stringify = stringify(HsBookingItemEntity.class)
.withProp(HsBookingItemEntity::getDebitor)
.withProp(HsBookingItemEntity::getProject)
.withProp(HsBookingItemEntity::getType)
.withProp(e -> e.getValidity().asString())
.withProp(HsBookingItemEntity::getCaption)
@ -83,10 +87,15 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
@Version
private int version;
@ManyToOne(optional = false)
@JoinColumn(name = "debitoruuid")
private HsOfficeDebitorEntity debitor;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "projectuuid")
private HsBookingProjectEntity project;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parentitemuuid")
private HsBookingItemEntity parentItem;
@NotNull
@Column(name = "type")
@Enumerated(EnumType.STRING)
private HsBookingItemType type;
@ -94,7 +103,7 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
@Builder.Default
@Type(PostgreSQLRangeType.class)
@Column(name = "validity", columnDefinition = "daterange")
private Range<LocalDate> validity = Range.emptyRange(LocalDate.class);
private Range<LocalDate> validity = Range.closedInfinite(LocalDate.now());
@Column(name = "caption")
private String caption;
@ -105,6 +114,13 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
@Column(columnDefinition = "resources")
private Map<String, Object> resources = new HashMap<>();
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true)
@JoinColumn(name="parentitemuuid", referencedColumnName="uuid")
private List<HsBookingItemEntity> subBookingItems;
@OneToOne(mappedBy="bookingItem")
private HsHostingAssetEntity relatedHostingAsset;
@Transient
private PatchableMapWrapper<Object> resourcesWrapper;
@ -132,6 +148,23 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
return upperInclusiveFromPostgresDateRange(getValidity());
}
@Override
public Map<String, Object> directProps() {
return resources;
}
@Override
public Object getContextValue(final String propName) {
final var v = resources.get(propName);
if (v!= null) {
return v;
}
if (parentItem!=null) {
return parentItem.getResources().get(propName);
}
return emptyMap();
}
@Override
public String toString() {
return stringify.apply(this);
@ -139,64 +172,59 @@ public class HsBookingItemEntity implements Stringifyable, RbacObject, Validatab
@Override
public String toShortString() {
return ofNullable(debitor).map(HsOfficeDebitorEntity::toShortString).orElse("D-???????") +
return ofNullable(relatedProject()).map(HsBookingProjectEntity::toShortString).orElse("D-???????-?") +
":" + caption;
}
@Override
public String getPropertiesName() {
return "resources";
private HsBookingProjectEntity relatedProject() {
if (project != null) {
return project;
}
return parentItem == null ? null : parentItem.relatedProject();
}
@Override
public Map<String, Object> getProperties() {
return resources;
public HsBookingProjectEntity getRelatedProject() {
return project != null ? project : parentItem.getRelatedProject();
}
public static RbacView rbac() {
return rbacViewFor("bookingItem", HsBookingItemEntity.class)
.withIdentityView(SQL.query("""
SELECT bookingItem.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName
FROM hs_booking_item bookingItem
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid
"""))
.withIdentityView(SQL.projection("caption"))
.withRestrictedViewOrderBy(SQL.expression("validity"))
.withUpdatableColumns("version", "caption", "validity", "resources")
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
dependsOnColumn("debitorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT ${columns}
FROM hs_office_relation debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid
"""),
NOT_NULL)
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
.toRole("global", ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
.toRole("global", ADMIN).grantPermission(DELETE)
.importEntityAlias("project", HsBookingProjectEntity.class, usingDefaultCase(),
dependsOnColumn("projectUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.toRole("project", ADMIN).grantPermission(INSERT)
.importEntityAlias("parentItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("parentItemUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.toRole("parentItem", ADMIN).grantPermission(INSERT)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("debitorRel", AGENT);
with.incomingSuperRole("project", AGENT);
with.incomingSuperRole("parentItem", AGENT);
})
.createSubRole(ADMIN, (with) -> {
with.incomingSuperRole("debitorRel", AGENT);
with.permission(UPDATE);
})
.createSubRole(AGENT)
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("debitorRel", TENANT);
with.outgoingSubRole("project", TENANT);
with.outgoingSubRole("parentItem", TENANT);
with.permission(SELECT);
})
.limitDiagramTo("bookingItem", "debitorRel", "global");
.limitDiagramTo("bookingItem", "project", "global");
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("6-hs-booking/601-booking-item/6013-hs-booking-item-rbac");
rbac().generateWithBaseFileName("6-hs-booking/630-booking-item/6303-hs-booking-item-rbac");
}
}

View File

@ -8,10 +8,11 @@ import java.util.UUID;
public interface HsBookingItemRepository extends Repository<HsBookingItemEntity, UUID> {
List<HsBookingItemEntity> findAll();
Optional<HsBookingItemEntity> findByUuid(final UUID bookingItemUuid);
List<HsBookingItemEntity> findAllByDebitorUuid(final UUID bookingItemUuid);
List<HsBookingItemEntity> findByCaption(String bookingItemCaption);
List<HsBookingItemEntity> findAllByProjectUuid(final UUID projectItemUuid);
HsBookingItemEntity save(HsBookingItemEntity current);

View File

@ -1,8 +1,35 @@
package net.hostsharing.hsadminng.hs.booking.item;
public enum HsBookingItemType {
import java.util.List;
import static java.util.Optional.ofNullable;
public enum HsBookingItemType implements Node {
PRIVATE_CLOUD,
CLOUD_SERVER,
MANAGED_SERVER,
MANAGED_WEBSPACE
CLOUD_SERVER(PRIVATE_CLOUD),
MANAGED_SERVER(PRIVATE_CLOUD),
MANAGED_WEBSPACE(MANAGED_SERVER);
private final HsBookingItemType parentItemType;
HsBookingItemType() {
this.parentItemType = null;
}
HsBookingItemType(final HsBookingItemType parentItemType) {
this.parentItemType = parentItemType;
}
@Override
public List<String> edges() {
return ofNullable(parentItemType)
.map(p -> (nodeName() + " *--> " + p.nodeName()))
.stream().toList();
}
@Override
public String nodeName() {
return "BI_" + name();
}
}

View File

@ -0,0 +1,9 @@
package net.hostsharing.hsadminng.hs.booking.item;
import java.util.List;
public interface Node {
String nodeName();
List<String> edges();
}

View File

@ -0,0 +1,84 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import org.apache.commons.lang3.BooleanUtils;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public class HsBookingItemEntityValidator extends HsEntityValidator<HsBookingItemEntity> {
public HsBookingItemEntityValidator(final ValidatableProperty<?, ?>... properties) {
super(properties);
}
@Override
public List<String> validateEntity(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"), super.validateProperties(bookingItem));
}
@Override
public List<String> validateContext(final HsBookingItemEntity bookingItem) {
return sequentiallyValidate(
() -> optionallyValidate(bookingItem.getParentItem()),
() -> validateAgainstSubEntities(bookingItem)
);
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null
? enrich(prefix(bookingItem.toShortString(), ""),
HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
: emptyList();
}
protected List<String> validateAgainstSubEntities(final HsBookingItemEntity bookingItem) {
return enrich(prefix(bookingItem.toShortString(), "resources"),
Stream.concat(
stream(propertyValidators)
.map(propDef -> propDef.validateTotals(bookingItem))
.flatMap(Collection::stream),
stream(propertyValidators)
.filter(ValidatableProperty::isTotalsValidator)
.map(prop -> validateMaxTotalValue(bookingItem, prop))
).filter(Objects::nonNull).toList());
}
// TODO.refa: convert into generic shape like multi-options validator
private static String validateMaxTotalValue(
final HsBookingItemEntity bookingItem,
final ValidatableProperty<?, ?> propDef) {
final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(bookingItem.getSubBookingItems()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getResources()))
.map(HsBookingItemEntityValidator::convertBooleanToInteger)
.map(HsBookingItemEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum);
final var maxValue = getIntegerValueWithDefault0(propDef, bookingItem.getResources());
if (propDef.thresholdPercentage() != null ) {
return totalValue > (maxValue * propDef.thresholdPercentage() / 100)
? "%s' maximum total is %d%s, but actual total %s is %d%s, which exceeds threshold of %d%%"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit, propDef.thresholdPercentage())
: null;
} else {
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total %s is %d%s"
.formatted(propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
}
private static Object convertBooleanToInteger(final Object value) {
return value instanceof Boolean ? BooleanUtils.toInteger((Boolean)value) : value;
}
}

View File

@ -1,12 +1,12 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -14,37 +14,44 @@ import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.MANAGED_WEBSPACE;
import static net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType.PRIVATE_CLOUD;
@UtilityClass
public class HsBookingItemEntityValidators {
public class HsBookingItemEntityValidatorRegistry {
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity, HsBookingItemType>> validators = new HashMap<>();
private static final Map<Enum<HsBookingItemType>, HsEntityValidator<HsBookingItemEntity>> validators = new HashMap<>();
static {
register(PRIVATE_CLOUD, new HsPrivateCloudBookingItemValidator());
register(CLOUD_SERVER, new HsCloudServerBookingItemValidator());
register(MANAGED_SERVER, new HsManagedServerBookingItemValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceBookingItemValidator());
}
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity, HsBookingItemType> validator) {
private static void register(final Enum<HsBookingItemType> type, final HsEntityValidator<HsBookingItemEntity> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsBookingItemEntity, HsBookingItemType> forType(final Enum<HsBookingItemType> type) {
public static HsEntityValidator<HsBookingItemEntity> forType(final Enum<HsBookingItemType> type) {
if ( validators.containsKey(type)) {
return validators.get(type);
}
throw new IllegalArgumentException("no validator found for type " + type);
}
public static Set<Enum<HsBookingItemType>> types() {
return validators.keySet();
}
public static HsBookingItemEntity valid(final HsBookingItemEntity entityToSave) {
final var violations = HsBookingItemEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
public static List<String> doValidate(final HsBookingItemEntity bookingItem) {
return HsEntityValidator.sequentiallyValidate(
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem));
}
public static HsBookingItemEntity validated(final HsBookingItemEntity entityToSave) {
MultiValidationException.throwIfNotEmpty(doValidate(entityToSave));
return entityToSave;
}
}

View File

@ -1,22 +1,28 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsCloudServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
class HsCloudServerBookingItemValidator extends HsBookingItemEntityValidator {
HsCloudServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
// @formatter:off
booleanProperty("active") .withDefault(true),
integerProperty("CPUs") .min( 1) .max( 32) .required(),
integerProperty("RAM").unit("GB") .min( 1) .max( 128) .required(),
integerProperty("SSD").unit("GB") .min( 0) .max( 1000) .step(25).required(), // (1)
integerProperty("HDD").unit("GB") .min( 0) .max( 4000) .step(250).withDefault(0),
integerProperty("Traffic").unit("GB") .min(250) .max(10000) .step(250).required(),
enumerationProperty("SLA-Infrastructure").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional()
// @formatter:on
);
// (q) We do have pre-existing CloudServers without SSD, just HDD, thus SSD starts with min=0.
// TODO.impl: Validation that SSD+HDD is at minimum 25 GB is missing.
// e.g. validationGroup("SSD", "HDD").min(0);
}
}

View File

@ -1,24 +1,22 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
class HsManagedServerBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerBookingItemValidator extends HsBookingItemEntityValidator {
HsManagedServerBookingItemValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").optional(),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").optional(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required().asTotalLimit().withThreshold(200),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).withDefault(0).asTotalLimit().withThreshold(200),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required().asTotalLimit().withThreshold(200),
enumerationProperty("SLA-Platform").values("BASIC", "EXT8H", "EXT4H", "EXT2H").withDefault("BASIC"),
booleanProperty("SLA-EMail").falseIf("SLA-Platform", "BASIC").withDefault(false),
booleanProperty("SLA-Maria").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-PgSQL").falseIf("SLA-Platform", "BASIC").optional(),
booleanProperty("SLA-Office").falseIf("SLA-Platform", "BASIC").optional(),

View File

@ -1,24 +1,103 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.IntegerProperty;
import org.apache.commons.lang3.function.TriFunction;
import java.util.List;
import static net.hostsharing.hsadminng.hs.validation.BooleanPropertyValidator.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationPropertyValidator.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.EMAIL_ADDRESS;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_DATABASE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MARIADB_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_DATABASE;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.PGSQL_USER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.UNIX_USER;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedWebspaceBookingItemValidator extends HsEntityValidator<HsBookingItemEntity, HsBookingItemType> {
class HsManagedWebspaceBookingItemValidator extends HsBookingItemEntityValidator {
public HsManagedWebspaceBookingItemValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").optional(),
integerProperty("Daemons").min(0).max(10).optional(),
booleanProperty("Online Office Server").optional()
integerProperty("Multi").min(1).max(100).step(1).withDefault(1)
.eachComprising( 25, unixUsers())
.eachComprising( 5, databaseUsers())
.eachComprising( 5, databases())
.eachComprising(250, eMailAddresses()),
integerProperty("Daemons").min(0).max(10).withDefault(0),
booleanProperty("Online Office Server").optional(),
enumerationProperty("SLA-Platform").values("BASIC", "EXT24H").withDefault("BASIC")
);
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> unixUsers() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = ofNullable(entity.getRelatedHostingAsset())
.map(ha -> ha.getSubHostingAssets().stream()
.filter(subAsset -> subAsset.getType() == UNIX_USER)
.count())
.orElse(0L);
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " unix users, but " + unixUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databaseUsers() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var dbUserCount = ofNullable(entity.getRelatedHostingAsset())
.map(ha -> ha.getSubHostingAssets().stream()
.filter(bi -> bi.getType() == PGSQL_USER || bi.getType() == MARIADB_USER )
.count())
.orElse(0L);
final long limitingValue = prop.getValue(entity.getResources());
if (dbUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " database users, but " + dbUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> databases() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = ofNullable(entity.getRelatedHostingAsset())
.map(ha -> ha.getSubHostingAssets().stream()
.filter(bi -> bi.getType()==PGSQL_USER || bi.getType()==MARIADB_USER )
.flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
.filter(subAsset -> subAsset.getType()==PGSQL_DATABASE || subAsset.getType()==MARIADB_DATABASE))
.count())
.orElse(0L);
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
}
return emptyList();
};
}
private static TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> eMailAddresses() {
return (final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var unixUserCount = ofNullable(entity.getRelatedHostingAsset())
.map(ha -> ha.getSubHostingAssets().stream()
.filter(bi -> bi.getType() == DOMAIN_MBOX_SETUP)
.flatMap(domainEMailSetup -> domainEMailSetup.getSubHostingAssets().stream()
.filter(subAsset -> subAsset.getType()==EMAIL_ADDRESS))
.count())
.orElse(0L);
final long limitingValue = prop.getValue(entity.getResources());
if (unixUserCount > factor*limitingValue) {
return List.of(prop.propertyName() + "=" + limitingValue + " allows at maximum " + limitingValue*factor + " databases, but " + unixUserCount + " found");
}
return emptyList();
};
}
}

View File

@ -0,0 +1,40 @@
package net.hostsharing.hsadminng.hs.booking.item.validators;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsPrivateCloudBookingItemValidator extends HsBookingItemEntityValidator {
HsPrivateCloudBookingItemValidator() {
super(
// @formatter:off
integerProperty("CPUs") .min( 1).max( 128).required().asTotalLimit(),
integerProperty("RAM").unit("GB") .min( 1).max( 512).required().asTotalLimit(),
integerProperty("SSD").unit("GB") .min( 25).max( 4000).step(25).required().asTotalLimit(),
integerProperty("HDD").unit("GB") .min( 0).max(16000).step(250).withDefault(0).asTotalLimit(),
integerProperty("Traffic").unit("GB") .min(250).max(40000).step(250).required().asTotalLimit(),
// Alternatively we could specify it similarly to "Multi" option but exclusively counting:
// integerProperty("Resource-Points") .min(4).max(100).required()
// .each("CPUs").countsAs(64)
// .each("RAM").countsAs(64)
// .each("SSD").countsAs(18)
// .each("HDD").countsAs(2)
// .each("Traffic").countsAs(1),
integerProperty("SLA-Infrastructure EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT8H"),
integerProperty("SLA-Infrastructure EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT4H"),
integerProperty("SLA-Infrastructure EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Infrastructure", "EXT2H"),
integerProperty("SLA-Platform EXT8H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT8H"),
integerProperty("SLA-Platform EXT4H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT4H"),
integerProperty("SLA-Platform EXT2H") .min( 0).max( 20).withDefault(0).asTotalLimitFor("SLA-Platform", "EXT2H"),
integerProperty("SLA-EMail") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Maria") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-PgSQL") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Office") .min( 0).max( 20).withDefault(0).asTotalLimit(),
integerProperty("SLA-Web") .min( 0).max( 20).withDefault(0).asTotalLimit()
// @formatter:on
);
}
}

View File

@ -0,0 +1,128 @@
package net.hostsharing.hsadminng.hs.booking.project;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorRepository;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjectsApi;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
@RestController
public class HsBookingProjectController implements HsBookingProjectsApi {
@Autowired
private Context context;
@Autowired
private Mapper mapper;
@Autowired
private HsBookingProjectRepository bookingProjectRepo;
@Autowired
private HsBookingDebitorRepository debitorRepo;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsBookingProjectResource>> listBookingProjectsByDebitorUuid(
final String currentUser,
final String assumedRoles,
final UUID debitorUuid) {
context.define(currentUser, assumedRoles);
final var entities = bookingProjectRepo.findAllByDebitorUuid(debitorUuid);
final var resources = mapper.mapList(entities, HsBookingProjectResource.class);
return ResponseEntity.ok(resources);
}
@Override
@Transactional
public ResponseEntity<HsBookingProjectResource> addBookingProject(
final String currentUser,
final String assumedRoles,
final HsBookingProjectInsertResource body) {
context.define(currentUser, assumedRoles);
final var entityToSave = mapper.map(body, HsBookingProjectEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = bookingProjectRepo.save(entityToSave);
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/booking/projects/{id}")
.buildAndExpand(saved.getUuid())
.toUri();
final var mapped = mapper.map(saved, HsBookingProjectResource.class);
return ResponseEntity.created(uri).body(mapped);
}
@Override
@Transactional(readOnly = true)
public ResponseEntity<HsBookingProjectResource> getBookingProjectByUuid(
final String currentUser,
final String assumedRoles,
final UUID bookingProjectUuid) {
context.define(currentUser, assumedRoles);
final var result = bookingProjectRepo.findByUuid(bookingProjectUuid);
return result
.map(bookingProjectEntity -> ResponseEntity.ok(
mapper.map(bookingProjectEntity, HsBookingProjectResource.class)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@Override
@Transactional
public ResponseEntity<Void> deleteBookingIemByUuid(
final String currentUser,
final String assumedRoles,
final UUID bookingProjectUuid) {
context.define(currentUser, assumedRoles);
final var result = bookingProjectRepo.deleteByUuid(bookingProjectUuid);
return result == 0
? ResponseEntity.notFound().build()
: ResponseEntity.noContent().build();
}
@Override
@Transactional
public ResponseEntity<HsBookingProjectResource> patchBookingProject(
final String currentUser,
final String assumedRoles,
final UUID bookingProjectUuid,
final HsBookingProjectPatchResource body) {
context.define(currentUser, assumedRoles);
final var current = bookingProjectRepo.findByUuid(bookingProjectUuid).orElseThrow();
new HsBookingProjectEntityPatcher(current).apply(body);
final var saved = bookingProjectRepo.save(current);
final var mapped = mapper.map(saved, HsBookingProjectResource.class);
return ResponseEntity.ok(mapped);
}
final BiConsumer<HsBookingProjectInsertResource, HsBookingProjectEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
if (resource.getDebitorUuid() != null) {
entity.setDebitor(debitorRepo.findByUuid(resource.getDebitorUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] debitorUuid %s not found".formatted(
resource.getDebitorUuid()))));
}
};
}

View File

@ -0,0 +1,114 @@
package net.hostsharing.hsadminng.hs.booking.project;
import lombok.*;
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import jakarta.persistence.*;
import java.io.IOException;
import java.util.UUID;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Builder
@Entity
@Table(name = "hs_booking_project_rv")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsBookingProjectEntity implements Stringifyable, RbacObject {
private static Stringify<HsBookingProjectEntity> stringify = stringify(HsBookingProjectEntity.class)
.withProp(HsBookingProjectEntity::getDebitor)
.withProp(HsBookingProjectEntity::getCaption)
.quotedValues(false);
@Id
@GeneratedValue
private UUID uuid;
@Version
private int version;
@ManyToOne(optional = false)
@JoinColumn(name = "debitoruuid")
private HsBookingDebitorEntity debitor;
@Column(name = "caption")
private String caption;
@Override
public String toString() {
return stringify.apply(this);
}
@Override
public String toShortString() {
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
":" + caption;
}
public static RbacView rbac() {
return rbacViewFor("project", HsBookingProjectEntity.class)
.withIdentityView(SQL.query("""
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName
FROM hs_booking_project bookingProject
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
"""))
.withRestrictedViewOrderBy(SQL.expression("caption"))
.withUpdatableColumns("version", "caption")
.importEntityAlias("debitor", HsOfficeDebitorEntity.class, usingDefaultCase(),
dependsOnColumn("debitorUuid"),
directlyFetchedByDependsOnColumn(),
NOT_NULL)
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class, usingCase(DEBITOR),
dependsOnColumn("debitorUuid"),
fetchedBySql("""
SELECT ${columns}
FROM hs_office_relation debitorRel
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = ${REF}.debitorUuid
"""),
NOT_NULL)
.toRole("debitorRel", ADMIN).grantPermission(INSERT)
.toRole("global", ADMIN).grantPermission(DELETE)
.createRole(OWNER, (with) -> {
with.incomingSuperRole("debitorRel", AGENT);
})
.createSubRole(ADMIN, (with) -> {
with.permission(UPDATE);
})
.createSubRole(AGENT)
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("debitorRel", TENANT);
with.permission(SELECT);
})
.limitDiagramTo("project", "debitorRel", "global");
}
public static void main(String[] args) throws IOException {
rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac");
}
}

View File

@ -0,0 +1,22 @@
package net.hostsharing.hsadminng.hs.booking.project;
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectPatchResource;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
public class HsBookingProjectEntityPatcher implements EntityPatcher<HsBookingProjectPatchResource> {
private final HsBookingProjectEntity entity;
public HsBookingProjectEntityPatcher(final HsBookingProjectEntity entity) {
this.entity = entity;
}
@Override
public void apply(final HsBookingProjectPatchResource resource) {
OptionalFromJson.of(resource.getCaption())
.ifPresent(entity::setCaption);
}
}

View File

@ -0,0 +1,21 @@
package net.hostsharing.hsadminng.hs.booking.project;
import org.springframework.data.repository.Repository;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public interface HsBookingProjectRepository extends Repository<HsBookingProjectEntity, UUID> {
Optional<HsBookingProjectEntity> findByUuid(final UUID bookingProjectUuid);
List<HsBookingProjectEntity> findByCaption(final String projectCaption);
List<HsBookingProjectEntity> findAllByDebitorUuid(final UUID bookingProjectUuid);
HsBookingProjectEntity save(HsBookingProjectEntity current);
int deleteByUuid(final UUID uuid);
long count();
}

View File

@ -1,5 +1,8 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRepository;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetsApi;
import net.hostsharing.hsadminng.context.Context;
@ -15,16 +18,20 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityNotFoundException;
import jakarta.persistence.PersistenceContext;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
import static net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators.valid;
@RestController
public class HsHostingAssetController implements HsHostingAssetsApi {
@PersistenceContext
private EntityManager em;
@Autowired
private Context context;
@ -34,6 +41,9 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
@Autowired
private HsHostingAssetRepository assetRepo;
@Autowired
private HsBookingItemRepository bookingItemRepo;
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<HsHostingAssetResource>> listAssets(
@ -46,7 +56,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
final var entities = assetRepo.findAllByCriteria(debitorUuid, parentAssetUuid, HsHostingAssetType.of(type));
final var resources = mapper.mapList(entities, HsHostingAssetResource.class);
final var resources = mapper.mapList(entities, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
return ResponseEntity.ok(resources);
}
@ -60,16 +70,22 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
context.define(currentUser, assumedRoles);
final var entityToSave = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var entity = mapper.map(body, HsHostingAssetEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
final var saved = assetRepo.save(valid(entityToSave));
final var mapped = new HostingAssetEntitySaveProcessor(entity)
.preprocessEntity()
.validateEntity()
.prepareForSave()
.saveUsing(assetRepo::save)
.validateContext()
.mapUsing(e -> mapper.map(e, HsHostingAssetResource.class))
.revampProperties();
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/hs/hosting/assets/{id}")
.buildAndExpand(saved.getUuid())
.buildAndExpand(mapped.getUuid())
.toUri();
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.created(uri).body(mapped);
}
@ -78,14 +94,14 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
public ResponseEntity<HsHostingAssetResource> getAssetByUuid(
final String currentUser,
final String assumedRoles,
final UUID serverUuid) {
final UUID assetUuid) {
context.define(currentUser, assumedRoles);
final var result = assetRepo.findByUuid(serverUuid);
final var result = assetRepo.findByUuid(assetUuid);
return result
.map(serverEntity -> ResponseEntity.ok(
mapper.map(serverEntity, HsHostingAssetResource.class)))
.map(assetEntity -> ResponseEntity.ok(
mapper.map(assetEntity, HsHostingAssetResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@ -94,10 +110,10 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
public ResponseEntity<Void> deleteAssetUuid(
final String currentUser,
final String assumedRoles,
final UUID serverUuid) {
final UUID assetUuid) {
context.define(currentUser, assumedRoles);
final var result = assetRepo.deleteByUuid(serverUuid);
final var result = assetRepo.deleteByUuid(assetUuid);
return result == 0
? ResponseEntity.notFound().build()
: ResponseEntity.noContent().build();
@ -108,26 +124,43 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
public ResponseEntity<HsHostingAssetResource> patchAsset(
final String currentUser,
final String assumedRoles,
final UUID serverUuid,
final UUID assetUuid,
final HsHostingAssetPatchResource body) {
context.define(currentUser, assumedRoles);
final var current = assetRepo.findByUuid(serverUuid).orElseThrow();
final var entity = assetRepo.findByUuid(assetUuid).orElseThrow();
new HsHostingAssetEntityPatcher(current).apply(body);
new HsHostingAssetEntityPatcher(em, entity).apply(body);
final var mapped = new HostingAssetEntitySaveProcessor(entity)
.preprocessEntity()
.validateEntity()
.prepareForSave()
.saveUsing(assetRepo::save)
.validateContext()
.mapUsing(e -> mapper.map(e, HsHostingAssetResource.class))
.revampProperties();
final var saved = assetRepo.save(valid(current));
final var mapped = mapper.map(saved, HsHostingAssetResource.class);
return ResponseEntity.ok(mapped);
}
final BiConsumer<HsHostingAssetInsertResource, HsHostingAssetEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
entity.putConfig(KeyValueMap.from(resource.getConfig()));
if (resource.getBookingItemUuid() != null) {
entity.setBookingItem(bookingItemRepo.findByUuid(resource.getBookingItemUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] bookingItemUuid %s not found".formatted(
resource.getBookingItemUuid()))));
}
if (resource.getParentAssetUuid() != null) {
entity.setParentAsset(assetRepo.findByUuid(resource.getParentAssetUuid())
.orElseThrow(() -> new EntityNotFoundException("ERROR: [400] parentAssetUuid %s not found".formatted(
resource.getParentAssetUuid()))));
}
};
@SuppressWarnings("unchecked")
final BiConsumer<HsHostingAssetEntity, HsHostingAssetResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource)
-> resource.setConfig(HostingAssetEntityValidatorRegistry.forType(entity.getType())
.revampProperties(entity, (Map<String, Object>) resource.getConfig()));
}

View File

@ -8,7 +8,8 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.validation.Validatable;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -17,38 +18,44 @@ import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.PostLoad;
import jakarta.persistence.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.Version;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
import static java.util.Collections.emptyMap;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inCaseOf;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.CaseDef.inOtherCases;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.ColumnValue.usingDefaultCase;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.DELETE;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
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.RbacUserReference.UserRole.CREATOR;
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;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@ -61,13 +68,14 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validatable<HsHostingAssetEntity, HsHostingAssetType> {
public class HsHostingAssetEntity implements Stringifyable, RbacObject, PropertiesProvider {
private static Stringify<HsHostingAssetEntity> stringify = stringify(HsHostingAssetEntity.class)
.withProp(HsHostingAssetEntity::getType)
.withProp(HsHostingAssetEntity::getIdentifier)
.withProp(HsHostingAssetEntity::getCaption)
.withProp(HsHostingAssetEntity::getParentAsset)
.withProp(HsHostingAssetEntity::getAssignedToAsset)
.withProp(HsHostingAssetEntity::getBookingItem)
.withProp(HsHostingAssetEntity::getConfig)
.quotedValues(false);
@ -79,20 +87,32 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
@Version
private int version;
@ManyToOne(optional = false)
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "bookingitemuuid")
private HsBookingItemEntity bookingItem;
@ManyToOne(optional = true)
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parentassetuuid")
private HsHostingAssetEntity parentAsset;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "assignedtoassetuuid")
private HsHostingAssetEntity assignedToAsset;
@Column(name = "type")
@Enumerated(EnumType.STRING)
private HsHostingAssetType type;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "alarmcontactuuid")
private HsOfficeContactEntity alarmContact;
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
@JoinColumn(name="parentassetuuid", referencedColumnName="uuid")
private List<HsHostingAssetEntity> subHostingAssets;
@Column(name = "identifier")
private String identifier; // vm1234, xyz00, example.org, xyz00_abc
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
@Column(name = "caption")
private String caption;
@ -106,24 +126,44 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
@Transient
private PatchableMapWrapper<Object> configWrapper;
@Transient
private boolean isLoaded;
@PostLoad
public void markAsLoaded() {
this.isLoaded = true;
}
public PatchableMapWrapper<Object> getConfig() {
return PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config );
}
public void putConfig(Map<String, Object> newConfg) {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfg);
public void putConfig(Map<String, Object> newConfig) {
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper; }, config).assign(newConfig);
}
@Override
public String getPropertiesName() {
return "config";
}
@Override
public Map<String, Object> getProperties() {
public Map<String, Object> directProps() {
return config;
}
@Override
public Object getContextValue(final String propName) {
final var v = config.get(propName);
if (v!= null) {
return v;
}
if (bookingItem!=null) {
return bookingItem.getResources().get(propName);
}
if (parentAsset!=null && parentAsset.getBookingItem()!=null) {
return parentAsset.getBookingItem().getResources().get(propName);
}
return emptyMap();
}
@Override
public String toString() {
return stringify.apply(this);
@ -136,48 +176,62 @@ public class HsHostingAssetEntity implements Stringifyable, RbacObject, Validata
public static RbacView rbac() {
return rbacViewFor("asset", HsHostingAssetEntity.class)
.withIdentityView(SQL.query("""
SELECT asset.uuid as uuid, bookingItemIV.idName || '-' || cleanIdentifier(asset.identifier) as idName
FROM hs_hosting_asset asset
JOIN hs_booking_item_iv bookingItemIV ON bookingItemIV.uuid = asset.bookingItemUuid
"""))
.withIdentityView(SQL.projection("identifier"))
.withRestrictedViewOrderBy(SQL.expression("identifier"))
.withUpdatableColumns("version", "caption", "config")
.withUpdatableColumns("version", "caption", "config", "assignedToAssetUuid", "alarmContactUuid")
.toRole(GLOBAL, ADMIN).grantPermission(INSERT) // TODO.impl: Why is this necessary to insert test data?
.importEntityAlias("bookingItem", HsBookingItemEntity.class, usingDefaultCase(),
dependsOnColumn("bookingItemUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.switchOnColumn("type",
inCaseOf(CLOUD_SERVER.name(),
then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)),
inCaseOf(MANAGED_SERVER.name(),
then -> then.toRole("bookingItem", AGENT).grantPermission(INSERT)),
inCaseOf(MANAGED_WEBSPACE.name(), then ->
then.importEntityAlias("parentServer", HsHostingAssetEntity.class, usingCase(MANAGED_SERVER),
.importEntityAlias("parentAsset", HsHostingAssetEntity.class, usingDefaultCase(),
dependsOnColumn("parentAssetUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.toRole("parentServer", ADMIN).grantPermission(INSERT)
.toRole("bookingItem", AGENT).grantPermission(INSERT)
),
inOtherCases(then -> {})
.toRole("parentAsset", ADMIN).grantPermission(INSERT)
.importEntityAlias("assignedToAsset", HsHostingAssetEntity.class, usingDefaultCase(),
dependsOnColumn("assignedToAssetUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.importEntityAlias("alarmContact", HsOfficeContactEntity.class, usingDefaultCase(),
dependsOnColumn("alarmContactUuid"),
directlyFetchedByDependsOnColumn(),
NULLABLE)
.switchOnColumn("type",
inCaseOf("DOMAIN_SETUP", then -> {
then.toRole(GLOBAL, GUEST).grantPermission(INSERT);
})
)
.createRole(OWNER, (with) -> {
with.owningUser(CREATOR);
with.incomingSuperRole(GLOBAL, ADMIN).unassumed(); // TODO.spec: replace by a better solution
with.incomingSuperRole("bookingItem", ADMIN);
with.incomingSuperRole("parentAsset", ADMIN);
with.permission(DELETE);
})
.createSubRole(ADMIN, (with) -> {
with.incomingSuperRole("bookingItem", AGENT);
with.incomingSuperRole("parentAsset", AGENT);
with.permission(UPDATE);
})
.createSubRole(AGENT, (with) -> {
with.outgoingSubRole("assignedToAsset", TENANT);
with.outgoingSubRole("alarmContact", REFERRER);
})
.createSubRole(TENANT, (with) -> {
with.outgoingSubRole("bookingItem", TENANT);
with.outgoingSubRole("parentAsset", TENANT);
with.incomingSuperRole("alarmContact", ADMIN);
with.permission(SELECT);
})
.limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentServer", "global");
.limitDiagramTo("asset", "bookingItem", "bookingItem.debitorRel", "parentAsset", "assignedToAsset", "alarmContact", "global");
}
public static void main(String[] args) throws IOException {

View File

@ -1,17 +1,21 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetPatchResource;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.mapper.EntityPatcher;
import net.hostsharing.hsadminng.mapper.KeyValueMap;
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
import jakarta.persistence.EntityManager;
import java.util.Optional;
public class HsHostingAssetEntityPatcher implements EntityPatcher<HsHostingAssetPatchResource> {
private final EntityManager em;
private final HsHostingAssetEntity entity;
public HsHostingAssetEntityPatcher(final HsHostingAssetEntity entity) {
HsHostingAssetEntityPatcher(final EntityManager em, final HsHostingAssetEntity entity) {
this.em = em;
this.entity = entity;
}
@ -21,5 +25,11 @@ public class HsHostingAssetEntityPatcher implements EntityPatcher<HsHostingAsset
.ifPresent(entity::setCaption);
Optional.ofNullable(resource.getConfig())
.ifPresent(r -> entity.getConfig().patch(KeyValueMap.from(resource.getConfig())));
OptionalFromJson.of(resource.getAlarmContactUuid())
// HOWTO: patch nullable JSON resource uuid to an ntity reference
.ifPresent(newValue -> entity.setAlarmContact(
Optional.ofNullable(newValue)
.map(uuid -> em.getReference(HsOfficeContactEntity.class, newValue))
.orElse(null)));
}
}

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HsHostingAssetEntityValidators;
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.api.HsHostingAssetPropsApi;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
import org.springframework.http.ResponseEntity;
@ -15,7 +15,7 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
@Override
public ResponseEntity<List<String>> listAssetTypes() {
final var resource = HsHostingAssetEntityValidators.types().stream()
final var resource = HostingAssetEntityValidatorRegistry.types().stream()
.map(Enum::name)
.toList();
return ResponseEntity.ok(resource);
@ -25,7 +25,8 @@ public class HsHostingAssetPropsController implements HsHostingAssetPropsApi {
public ResponseEntity<List<Object>> listAssetTypeProps(
final HsHostingAssetTypeResource assetType) {
final var propValidators = HsHostingAssetEntityValidators.forType(HsHostingAssetType.of(assetType));
final Enum<HsHostingAssetType> type = HsHostingAssetType.of(assetType);
final var propValidators = HostingAssetEntityValidatorRegistry.forType(type);
final List<Map<String, Object>> resource = propValidators.properties();
return ResponseEntity.ok(toListOfObjects(resource));
}

View File

@ -10,18 +10,33 @@ import java.util.UUID;
public interface HsHostingAssetRepository extends Repository<HsHostingAssetEntity, UUID> {
List<HsHostingAssetEntity> findAll();
Optional<HsHostingAssetEntity> findByUuid(final UUID serverUuid);
@Query("""
SELECT asset FROM HsHostingAssetEntity asset
WHERE (:debitorUuid IS NULL OR asset.bookingItem.debitor.uuid = :debitorUuid)
AND (:parentAssetUuid IS NULL OR asset.parentAsset.uuid = :parentAssetUuid)
AND (:type IS NULL OR :type = CAST(asset.type AS String))
""")
List<HsHostingAssetEntity> findAllByCriteriaImpl(UUID debitorUuid, UUID parentAssetUuid, String type);
default List<HsHostingAssetEntity> findAllByCriteria(final UUID debitorUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
return findAllByCriteriaImpl(debitorUuid, parentAssetUuid, HsHostingAssetType.asString(type));
List<HsHostingAssetEntity> findByIdentifier(String assetIdentifier);
@Query(value = """
select ha.uuid,
ha.alarmcontactuuid,
ha.assignedtoassetuuid,
ha.bookingitemuuid,
ha.caption,
ha.config,
ha.identifier,
ha.parentassetuuid,
ha.type,
ha.version
from hs_hosting_asset_rv ha
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid
where (:projectUuid is null or bi.projectuuid=:projectUuid)
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
and (:type is null or :type=cast(ha.type as text))
""", nativeQuery = true)
// The JPQL query did not generate "left join" but just "join".
// I also optimized the query by not using the _rv for hs_booking_item and hs_hosting_asset, only for hs_hosting_asset_rv.
List<HsHostingAssetEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
default List<HsHostingAssetEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
}
HsHostingAssetEntity save(HsHostingAssetEntity current);

View File

@ -1,30 +1,205 @@
package net.hostsharing.hsadminng.hs.hosting.asset;
import lombok.AllArgsConstructor;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemType;
import net.hostsharing.hsadminng.hs.booking.item.Node;
public enum HsHostingAssetType {
CLOUD_SERVER, // named e.g. vm1234
MANAGED_SERVER, // named e.g. vm1234
MANAGED_WEBSPACE(MANAGED_SERVER), // named eg. xyz00
UNIX_USER(MANAGED_WEBSPACE), // named e.g. xyz00-abc
DOMAIN_SETUP(UNIX_USER), // named e.g. example.org
import javax.naming.NamingException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import static net.hostsharing.hsadminng.hs.hosting.asset.EntityTypeRelation.*;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationPolicy.OPTIONAL;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationPolicy.REQUIRED;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.ASSIGNED_TO_ASSET;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.BOOKING_ITEM;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.RelationType.PARENT_ASSET;
public enum HsHostingAssetType implements Node {
SAME_TYPE, // pseudo-type for recursive references
CLOUD_SERVER( // named e.g. vm1234
inGroup("Server"),
requires(HsBookingItemType.CLOUD_SERVER)),
MANAGED_SERVER( // named e.g. vm1234
inGroup("Server"),
requires(HsBookingItemType.MANAGED_SERVER)),
MANAGED_WEBSPACE( // named eg. xyz00
inGroup("Webspace"),
requires(HsBookingItemType.MANAGED_WEBSPACE),
optionalParent(MANAGED_SERVER)),
UNIX_USER( // named e.g. xyz00-abc
inGroup("Webspace"),
requiredParent(MANAGED_WEBSPACE)),
EMAIL_ALIAS( // named e.g. xyz00-abc
inGroup("Webspace"),
requiredParent(MANAGED_WEBSPACE)),
DOMAIN_SETUP( // named e.g. example.org
inGroup("Domain"),
optionalParent(SAME_TYPE)
),
DOMAIN_DNS_SETUP( // named e.g. example.org
inGroup("Domain"),
requiredParent(DOMAIN_SETUP),
assignedTo(MANAGED_WEBSPACE)),
DOMAIN_HTTP_SETUP( // named e.g. example.org
inGroup("Domain"),
requiredParent(DOMAIN_SETUP),
assignedTo(UNIX_USER)),
DOMAIN_SMTP_SETUP( // named e.g. example.org
inGroup("Domain"),
requiredParent(DOMAIN_SETUP),
assignedTo(MANAGED_WEBSPACE)),
DOMAIN_MBOX_SETUP( // named e.g. example.org
inGroup("Domain"),
requiredParent(DOMAIN_SETUP),
assignedTo(MANAGED_WEBSPACE)),
// TODO.spec: SECURE_MX
EMAIL_ALIAS(MANAGED_WEBSPACE), // named e.g. xyz00-abc
EMAIL_ADDRESS(DOMAIN_SETUP), // named e.g. sample@example.org
PGSQL_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc
PGSQL_DATABASE(MANAGED_WEBSPACE), // named e.g. xyz00_abc, TODO.spec: or PGSQL_USER?
MARIADB_USER(MANAGED_WEBSPACE), // named e.g. xyz00_abc
MARIADB_DATABASE(MANAGED_WEBSPACE); // named e.g. xyz00_abc, TODO.spec: or MARIADB_USER?
EMAIL_ADDRESS( // named e.g. sample@example.org
inGroup("Domain"),
requiredParent(DOMAIN_MBOX_SETUP)),
public final HsHostingAssetType parentAssetType;
PGSQL_INSTANCE( // TODO.spec: identifier to be specified
inGroup("PostgreSQL"),
requiredParent(MANAGED_SERVER)),
HsHostingAssetType(final HsHostingAssetType parentAssetType) {
this.parentAssetType = parentAssetType;
PGSQL_USER( // named e.g. xyz00_abc
inGroup("PostgreSQL"),
requiredParent(PGSQL_INSTANCE),
assignedTo(MANAGED_WEBSPACE)),
PGSQL_DATABASE( // named e.g. xyz00_abc
inGroup("PostgreSQL"),
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or PGSQL_USER?
assignedTo(PGSQL_INSTANCE)), // TODO.spec: or swapping parent+assignedTo?
MARIADB_INSTANCE( // TODO.spec: identifier to be specified
inGroup("MariaDB"),
requiredParent(MANAGED_SERVER)), // TODO.spec: or MANAGED_WEBSPACE?
MARIADB_USER( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MARIADB_INSTANCE),
assignedTo(MANAGED_WEBSPACE)),
MARIADB_DATABASE( // named e.g. xyz00_abc
inGroup("MariaDB"),
requiredParent(MANAGED_WEBSPACE), // TODO.spec: or MARIADB_USER?
assignedTo(MARIADB_INSTANCE)), // TODO.spec: or swapping parent+assignedTo?
IP_NUMBER(
inGroup("Server"),
assignedTo(CLOUD_SERVER),
assignedTo(MANAGED_SERVER),
assignedTo(MANAGED_WEBSPACE)
);
private final String groupName;
private final EntityTypeRelation<?, ?>[] relations;
HsHostingAssetType(
final String groupName,
final EntityTypeRelation<?, ?>... relations
) {
this.groupName = groupName;
this.relations = relations;
}
HsHostingAssetType() {
this(null);
this.groupName = null;
this.relations = null;
}
/// just syntactic sugar
private static String inGroup(final String groupName) {
return groupName;
}
// TODO.refa: try to get rid of the following similar methods:
public RelationPolicy bookingItemPolicy() {
return stream(relations)
.filter(r -> r.relationType == BOOKING_ITEM)
.map(r -> r.relationPolicy)
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(RelationPolicy.FORBIDDEN);
}
public HsBookingItemType bookingItemType() {
return stream(relations)
.filter(r -> r.relationType == BOOKING_ITEM)
.map(r -> HsBookingItemType.valueOf(r.relatedType(this).toString()))
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(null);
}
public RelationPolicy parentAssetPolicy() {
return stream(relations)
.filter(r -> r.relationType == PARENT_ASSET)
.map(r -> r.relationPolicy)
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(RelationPolicy.FORBIDDEN);
}
public HsHostingAssetType parentAssetType() {
return stream(relations)
.filter(r -> r.relationType == PARENT_ASSET)
.map(r -> HsHostingAssetType.valueOf(r.relatedType(this).toString()))
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(null);
}
public RelationPolicy assignedToAssetPolicy() {
return stream(relations)
.filter(r -> r.relationType == ASSIGNED_TO_ASSET)
.map(r -> r.relationPolicy)
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(RelationPolicy.FORBIDDEN);
}
public HsHostingAssetType assignedToAssetType() {
return stream(relations)
.filter(r -> r.relationType == ASSIGNED_TO_ASSET)
.map(r -> HsHostingAssetType.valueOf(r.relatedType(this).toString()))
.reduce(HsHostingAssetType::onlyASingleElementExpectedException)
.orElse(null);
}
private static <X> X onlyASingleElementExpectedException(Object a, Object b) {
throw new IllegalStateException("Only a single element expected to match criteria.");
}
@Override
public List<String> edges() {
return stream(relations)
.map(r -> nodeName() + r.edge + r.relatedType(this).nodeName())
.toList();
}
@Override
public String nodeName() {
return "HA_" + name();
}
public static <T extends Enum<?>> HsHostingAssetType of(final T value) {
@ -34,4 +209,148 @@ public enum HsHostingAssetType {
static String asString(final HsHostingAssetType type) {
return type == null ? null : type.name();
}
private static String renderAsPlantUML(final String caption, final Set<String> includedHostingGroups) {
final String bookingNodes = stream(HsBookingItemType.values())
.map(t -> " entity " + t.nodeName())
.collect(joining("\n"));
final String hostingGroups = includedHostingGroups.stream().sorted()
.map(HsHostingAssetType::generateGroup)
.collect(joining("\n"));
final String hostingAssetNodes = stream(HsHostingAssetType.values())
.filter(t -> t.isInGroups(includedHostingGroups))
.map(t -> "entity " + t.nodeName())
.collect(joining("\n"));
final String bookingItemEdges = stream(HsBookingItemType.values())
.map(HsBookingItemType::edges)
.flatMap(Collection::stream)
.collect(joining("\n"));
final String hostingAssetEdges = stream(HsHostingAssetType.values())
.filter(t -> t.isInGroups(includedHostingGroups))
.map(HsHostingAssetType::edges)
.flatMap(Collection::stream)
.collect(joining("\n"));
return """
### %{caption}
```plantuml
@startuml
left to right direction
package Booking #feb28c {
%{bookingNodes}
}
package Hosting #feb28c{
%{hostingGroups}
}
%{bookingItemEdges}
%{hostingAssetEdges}
package Legend #white {
SUB_ENTITY1 *--> REQUIRED_PARENT_ENTITY
SUB_ENTITY2 *..> OPTIONAL_PARENT_ENTITY
ASSIGNED_ENTITY1 o--> REQUIRED_ASSIGNED_TO_ENTITY1
ASSIGNED_ENTITY2 o..> OPTIONAL_ASSIGNED_TO_ENTITY2
}
Booking -down[hidden]->Legend
```
"""
.replace("%{caption}", caption)
.replace("%{bookingNodes}", bookingNodes)
.replace("%{hostingGroups}", hostingGroups)
.replace("%{hostingAssetNodeStyles}", hostingAssetNodes)
.replace("%{bookingItemEdges}", bookingItemEdges)
.replace("%{hostingAssetEdges}", hostingAssetEdges);
}
private boolean isInGroups(final Set<String> assetGroups) {
return groupName != null && assetGroups.contains(groupName);
}
private static String generateGroup(final String group) {
return " package " + group + " #99bcdb {\n"
+ stream(HsHostingAssetType.values())
.filter(t -> group.equals(t.groupName))
.map(t -> " entity " + t.nodeName())
.collect(joining("\n"))
+ "\n }\n";
}
static String renderAsEmbeddedPlantUml() {
final var markdown = new StringBuilder("""
## HostingAsset Type Structure
""");
// rendering all types in a single diagram is currently ignored
renderAsPlantUML("Domain", stream(HsHostingAssetType.values())
.filter(t -> t.groupName != null)
.map(t -> t.groupName)
.collect(toSet()));
markdown.append(renderAsPlantUML("Domain", Set.of("Domain", "Webspace", "Server")))
.append(renderAsPlantUML("MariaDB", Set.of("MariaDB", "Webspace", "Server")))
.append(renderAsPlantUML("PostgreSQL", Set.of("PostgreSQL", "Webspace", "Server")));
markdown.append("""
This code generated was by %{this}.main, do not amend manually.
"""
.replace("%{this}", HsHostingAssetType.class.getSimpleName()));
return markdown.toString();
}
public static void main(final String[] args) throws IOException, NamingException {
Files.writeString(
Path.of("doc/hs-hosting-asset-type-structure.md"),
renderAsEmbeddedPlantUml(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
public enum RelationPolicy {
FORBIDDEN, OPTIONAL, REQUIRED
}
public enum RelationType {
BOOKING_ITEM,
PARENT_ASSET,
ASSIGNED_TO_ASSET
}
}
@AllArgsConstructor
class EntityTypeRelation<E, T extends Node> {
final HsHostingAssetType.RelationPolicy relationPolicy;
final HsHostingAssetType.RelationType relationType;
final Function<HsHostingAssetEntity, E> getter;
private final T relatedType;
final String edge;
public T relatedType(final HsHostingAssetType referringType) {
//noinspection unchecked
return relatedType == HsHostingAssetType.SAME_TYPE ? (T) referringType : relatedType;
}
static EntityTypeRelation<HsBookingItemEntity, HsBookingItemType> requires(final HsBookingItemType bookingItemType) {
return new EntityTypeRelation<>(REQUIRED, BOOKING_ITEM, HsHostingAssetEntity::getBookingItem, bookingItemType, " *==> ");
}
static EntityTypeRelation<HsHostingAssetEntity, HsHostingAssetType> optionalParent(final HsHostingAssetType hostingAssetType) {
return new EntityTypeRelation<>(OPTIONAL, PARENT_ASSET, HsHostingAssetEntity::getParentAsset, hostingAssetType, " o..> ");
}
static EntityTypeRelation<HsHostingAssetEntity, HsHostingAssetType> requiredParent(final HsHostingAssetType hostingAssetType) {
return new EntityTypeRelation<>(REQUIRED, PARENT_ASSET, HsHostingAssetEntity::getParentAsset, hostingAssetType, " *==> ");
}
static EntityTypeRelation<HsHostingAssetEntity, HsHostingAssetType> assignedTo(final HsHostingAssetType hostingAssetType) {
return new EntityTypeRelation<>(REQUIRED, ASSIGNED_TO_ASSET, HsHostingAssetEntity::getAssignedToAsset, hostingAssetType, " o..> ");
}
}

View File

@ -0,0 +1,86 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.Map;
import java.util.function.Function;
/**
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsHostingAssetEntity into a readable API.
*/
public class HostingAssetEntitySaveProcessor {
private final HsEntityValidator<HsHostingAssetEntity> validator;
private String expectedStep = "preprocessEntity";
private HsHostingAssetEntity entity;
private HsHostingAssetResource resource;
public HostingAssetEntitySaveProcessor(final HsHostingAssetEntity entity) {
this.entity = entity;
this.validator = HostingAssetEntityValidatorRegistry.forType(entity.getType());
}
/// initial step allowing to set default values before any validations
public HostingAssetEntitySaveProcessor preprocessEntity() {
step("preprocessEntity", "validateEntity");
validator.preprocessEntity(entity);
return this;
}
/// validates the entity itself including its properties
public HostingAssetEntitySaveProcessor validateEntity() {
step("validateEntity", "prepareForSave");
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
return this;
}
/// hashing passwords etc.
@SuppressWarnings("unchecked")
public HostingAssetEntitySaveProcessor prepareForSave() {
step("prepareForSave", "saveUsing");
validator.prepareProperties(entity);
return this;
}
public HostingAssetEntitySaveProcessor 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 HostingAssetEntitySaveProcessor validateContext() {
step("validateContext", "mapUsing");
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
return this;
}
/// maps entity to JSON resource representation
public HostingAssetEntitySaveProcessor mapUsing(
final Function<HsHostingAssetEntity, HsHostingAssetResource> mapFunction) {
step("mapUsing", "revampProperties");
resource = mapFunction.apply(entity);
return this;
}
/// 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

@ -0,0 +1,221 @@
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.booking.item.validators.HsBookingItemEntityValidatorRegistry;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import net.hostsharing.hsadminng.hs.validation.ValidatableProperty;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static java.util.Arrays.stream;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
public abstract class HostingAssetEntityValidator extends HsEntityValidator<HsHostingAssetEntity> {
static final ValidatableProperty<?, ?>[] NO_EXTRA_PROPERTIES = new ValidatableProperty<?, ?>[0];
private final ReferenceValidator<HsBookingItemEntity, HsBookingItemType> bookingItemReferenceValidation;
private final ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> parentAssetReferenceValidation;
private final ReferenceValidator<HsHostingAssetEntity, HsHostingAssetType> assignedToAssetReferenceValidation;
private final HostingAssetEntityValidator.AlarmContact alarmContactValidation;
HostingAssetEntityValidator(
final HsHostingAssetType assetType,
final AlarmContact alarmContactValidation,
final ValidatableProperty<?, ?>... properties) {
super(properties);
this.bookingItemReferenceValidation = new ReferenceValidator<>(
assetType.bookingItemPolicy(),
assetType.bookingItemType(),
HsHostingAssetEntity::getBookingItem,
HsBookingItemEntity::getType);
this.parentAssetReferenceValidation = new ReferenceValidator<>(
assetType.parentAssetPolicy(),
assetType.parentAssetType(),
HsHostingAssetEntity::getParentAsset,
HsHostingAssetEntity::getType);
this.assignedToAssetReferenceValidation = new ReferenceValidator<>(
assetType.assignedToAssetPolicy(),
assetType.assignedToAssetType(),
HsHostingAssetEntity::getAssignedToAsset,
HsHostingAssetEntity::getType);
this.alarmContactValidation = alarmContactValidation;
}
@Override
public List<String> validateEntity(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate(
() -> validateEntityReferencesAndProperties(assetEntity),
() -> validateIdentifierPattern(assetEntity)
);
}
@Override
public List<String> validateContext(final HsHostingAssetEntity assetEntity) {
return sequentiallyValidate(
() -> optionallyValidate(assetEntity.getBookingItem()),
() -> optionallyValidate(assetEntity.getParentAsset()),
() -> validateAgainstSubEntities(assetEntity)
);
}
private List<String> validateEntityReferencesAndProperties(final HsHostingAssetEntity assetEntity) {
return Stream.of(
validateReferencedEntity(assetEntity, "bookingItem", bookingItemReferenceValidation::validate),
validateReferencedEntity(assetEntity, "parentAsset", parentAssetReferenceValidation::validate),
validateReferencedEntity(assetEntity, "assignedToAsset", assignedToAssetReferenceValidation::validate),
validateReferencedEntity(assetEntity, "alarmContact", alarmContactValidation::validate),
validateProperties(assetEntity))
.filter(Objects::nonNull)
.flatMap(List::stream)
.filter(Objects::nonNull)
.toList();
}
private List<String> validateReferencedEntity(
final HsHostingAssetEntity assetEntity,
final String referenceFieldName,
final BiFunction<HsHostingAssetEntity, String, List<String>> validator) {
return enrich(prefix(assetEntity.toShortString()), validator.apply(assetEntity, referenceFieldName));
}
private List<String> validateProperties(final HsHostingAssetEntity assetEntity) {
return enrich(prefix(assetEntity.toShortString(), "config"), super.validateProperties(assetEntity));
}
private static List<String> optionallyValidate(final HsHostingAssetEntity assetEntity) {
return assetEntity != null
? enrich(
prefix(assetEntity.toShortString(), "parentAsset"),
HostingAssetEntityValidatorRegistry.forType(assetEntity.getType()).validateContext(assetEntity))
: emptyList();
}
private static List<String> optionallyValidate(final HsBookingItemEntity bookingItem) {
return bookingItem != null
? enrich(
prefix(bookingItem.toShortString(), "bookingItem"),
HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
: emptyList();
}
protected List<String> validateAgainstSubEntities(final HsHostingAssetEntity assetEntity) {
return enrich(
prefix(assetEntity.toShortString(), "config"),
stream(propertyValidators)
.filter(ValidatableProperty::isTotalsValidator)
.map(prop -> validateMaxTotalValue(assetEntity, prop))
.filter(Objects::nonNull)
.toList());
}
// TODO.test: check, if there are any hosting assets which need this validation at all
private String validateMaxTotalValue(
final HsHostingAssetEntity hostingAsset,
final ValidatableProperty<?, ?> propDef) {
final var propName = propDef.propertyName();
final var propUnit = ofNullable(propDef.unit()).map(u -> " " + u).orElse("");
final var totalValue = ofNullable(hostingAsset.getSubHostingAssets()).orElse(emptyList())
.stream()
.map(subItem -> propDef.getValue(subItem.getConfig()))
.map(HsEntityValidator::toIntegerWithDefault0)
.reduce(0, Integer::sum);
final var maxValue = getIntegerValueWithDefault0(propDef, hostingAsset.getConfig());
return totalValue > maxValue
? "%s' maximum total is %d%s, but actual total %s is %d%s".formatted(
propName, maxValue, propUnit, propName, totalValue, propUnit)
: null;
}
private List<String> validateIdentifierPattern(final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = identifierPattern(assetEntity);
if (assetEntity.getIdentifier() == null ||
!expectedIdentifierPattern.matcher(assetEntity.getIdentifier()).matches()) {
return List.of(
"'identifier' expected to match '" + expectedIdentifierPattern + "', but is '" + assetEntity.getIdentifier()
+ "'");
}
return Collections.emptyList();
}
protected abstract Pattern identifierPattern(HsHostingAssetEntity assetEntity);
static class ReferenceValidator<S, T> {
private final HsHostingAssetType.RelationPolicy policy;
private final T referencedEntityType;
private final Function<HsHostingAssetEntity, S> referencedEntityGetter;
private final Function<S, T> referencedEntityTypeGetter;
public ReferenceValidator(
final HsHostingAssetType.RelationPolicy policy,
final T subEntityType,
final Function<HsHostingAssetEntity, S> referencedEntityGetter,
final Function<S, T> referencedEntityTypeGetter) {
this.policy = policy;
this.referencedEntityType = subEntityType;
this.referencedEntityGetter = referencedEntityGetter;
this.referencedEntityTypeGetter = referencedEntityTypeGetter;
}
public ReferenceValidator(
final HsHostingAssetType.RelationPolicy policy,
final Function<HsHostingAssetEntity, S> referencedEntityGetter) {
this.policy = policy;
this.referencedEntityType = null;
this.referencedEntityGetter = referencedEntityGetter;
this.referencedEntityTypeGetter = e -> null;
}
List<String> validate(final HsHostingAssetEntity assetEntity, final String referenceFieldName) {
final var actualEntity = referencedEntityGetter.apply(assetEntity);
final var actualEntityType = actualEntity != null ? referencedEntityTypeGetter.apply(actualEntity) : null;
switch (policy) {
case REQUIRED:
if (actualEntityType != referencedEntityType) {
return List.of(actualEntityType == null
? referenceFieldName + "' must be of type " + referencedEntityType + " but is null"
: referenceFieldName + "' must be of type " + referencedEntityType + " but is of type " + actualEntityType);
}
break;
case OPTIONAL:
if (actualEntityType != null && actualEntityType != referencedEntityType) {
return List.of(referenceFieldName + "' must be null or of type " + referencedEntityType + " but is of type "
+ actualEntityType);
}
break;
case FORBIDDEN:
if (actualEntityType != null) {
return List.of(referenceFieldName + "' must be null but is of type " + actualEntityType);
}
break;
}
return emptyList();
}
}
static class AlarmContact extends ReferenceValidator<HsOfficeContactEntity, Enum<?>> {
AlarmContact(final HsHostingAssetType.RelationPolicy policy) {
super(policy, HsHostingAssetEntity::getAlarmContact);
}
static AlarmContact isOptional() {
return new AlarmContact(HsHostingAssetType.RelationPolicy.OPTIONAL);
}
}
}

View File

@ -0,0 +1,57 @@
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 net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.*;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.*;
public class HostingAssetEntityValidatorRegistry {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity>> validators = new HashMap<>();
static {
// HOWTO: add (register) new HsHostingAssetType-specific validators
register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
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());
register(DOMAIN_HTTP_SETUP, new HsDomainHttpSetupHostingAssetValidator());
register(DOMAIN_SMTP_SETUP, new HsDomainSmtpSetupHostingAssetValidator());
register(DOMAIN_MBOX_SETUP, new HsDomainMboxSetupHostingAssetValidator());
register(EMAIL_ADDRESS, new HsEMailAddressHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity> forType(final Enum<HsHostingAssetType> type) {
if ( validators.containsKey(type)) {
return validators.get(type);
}
throw new IllegalArgumentException("no validator found for type " + type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
@SuppressWarnings("unchecked")
private static Map<String, Object> asMap(final HsHostingAssetResource resource) {
if (resource.getConfig() instanceof Map map) {
return map;
}
throw new IllegalArgumentException("expected a Map, but got a " + resource.getConfig().getClass());
}
}

View File

@ -1,20 +1,22 @@
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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
import java.util.regex.Pattern;
class HsCloudServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
public HsCloudServerHostingAssetValidator() {
class HsCloudServerHostingAssetValidator extends HostingAssetEntityValidator {
HsCloudServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
);
CLOUD_SERVER,
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$");
}
}

View File

@ -0,0 +1,110 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import lombok.SneakyThrows;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
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.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
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 HostingAssetEntityValidator {
// 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;
public static final String IDENTIFIER_SUFFIX = "|DNS";
HsDomainDnsSetupHostingAssetValidator() {
super(
DOMAIN_DNS_SETUP,
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("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX));
}
}
@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", fqdn(assetEntity));
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}", fqdn(assetEntity))
.replace("{ttl}", getPropertyValue(assetEntity, "TTL"))
.replace("{userRRs}", getPropertyValues(assetEntity, "user-RR") );
}
private String fqdn(final HsHostingAssetEntity assetEntity) {
return assetEntity.getIdentifier().substring(0, assetEntity.getIdentifier().length()-IDENTIFIER_SUFFIX.length());
}
}

View File

@ -0,0 +1,56 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
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.StringProperty.stringProperty;
class HsDomainHttpSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String IDENTIFIER_SUFFIX = "|HTTP";
public static final String FILESYSTEM_PATH = "^/";
public static final String PARTIAL_DOMAIN_NAME_REGEX = "(?!-)[A-Za-z0-9-]{1,63}(?<!-)";
HsDomainHttpSetupHostingAssetValidator() {
super(
DOMAIN_HTTP_SETUP,
AlarmContact.isOptional(),
booleanProperty("htdocsfallback").withDefault(true),
booleanProperty("indexes").withDefault(true),
booleanProperty("cgi").withDefault(true),
booleanProperty("passenger").withDefault(true),
booleanProperty("passenger-errorpage").withDefault(false),
booleanProperty("fastcgi").withDefault(true),
booleanProperty("autoconfig").withDefault(true),
booleanProperty("greylisting").withDefault(true),
booleanProperty("includes").withDefault(true),
booleanProperty("letsencrypt").withDefault(true),
booleanProperty("multiviews").withDefault(true),
stringProperty("fcgi-php-bin").matchesRegEx(FILESYSTEM_PATH).provided("/usr/lib/cgi-bin/php").withDefault("/usr/lib/cgi-bin/php"),
stringProperty("passenger-nodejs").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/node").withDefault("/usr/bin/node"),
stringProperty("passenger-python").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/python3").withDefault("/usr/bin/python3"),
stringProperty("passenger-ruby").matchesRegEx(FILESYSTEM_PATH).provided("/usr/bin/ruby").withDefault("/usr/bin/ruby"),
arrayOf(
stringProperty("subdomains").matchesRegEx(PARTIAL_DOMAIN_NAME_REGEX).required()
).optional());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX));
}
}
}

View File

@ -0,0 +1,34 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
class HsDomainMboxSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String IDENTIFIER_SUFFIX = "|MBOX";
HsDomainMboxSetupHostingAssetValidator() {
super(
DOMAIN_MBOX_SETUP,
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX));
}
}
}

View File

@ -0,0 +1,57 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.List;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SETUP;
class HsDomainSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}";
private final Pattern identifierPattern;
HsDomainSetupHostingAssetValidator() {
super( DOMAIN_SETUP,
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
this.identifierPattern = Pattern.compile(FQDN_REGEX);
}
@Override
public List<String> validateEntity(final HsHostingAssetEntity assetEntity) {
// TODO.impl: for newly created entities, check the permission of setting up a domain
//
// reject, if the domain is any of these:
// hostsharing.com|net|org|coop, // just to be on the safe side
// [^.}+, // top-level-domain
// co.uk, org.uk, gov.uk, ac.uk, sch.uk,
// com.au, net.au, org.au, edu.au, gov.au, asn.au, id.au,
// co.jp, ne.jp, or.jp, ac.jp, go.jp,
// com.cn, net.cn, org.cn, gov.cn, edu.cn, ac.cn,
// com.br, net.br, org.br, gov.br, edu.br, mil.br, art.br,
// co.in, net.in, org.in, gen.in, firm.in, ind.in,
// com.mx, net.mx, org.mx, gob.mx, edu.mx,
// gov.it, edu.it,
// co.nz, net.nz, org.nz, govt.nz, ac.nz, school.nz, geek.nz, kiwi.nz,
// co.kr, ne.kr, or.kr, go.kr, re.kr, pe.kr
//
// allow if
// - user has Admin/Agent-role for all its sub-domains and the direct parent-Domain which are set up at at Hostsharing
// - domain has DNS zone with TXT record approval
// - parent-domain has DNS zone with TXT record approval
//
// TXT-Record check:
// new InitialDirContext().getAttributes("dns:_netblocks.google.com", new String[] { "TXT"}).get("TXT").getAll();
return super.validateEntity(assetEntity);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return identifierPattern;
}
}

View File

@ -0,0 +1,34 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import java.util.regex.Pattern;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
class HsDomainSmtpSetupHostingAssetValidator extends HostingAssetEntityValidator {
public static final String IDENTIFIER_SUFFIX = "|SMTP";
HsDomainSmtpSetupHostingAssetValidator() {
super(
DOMAIN_SMTP_SETUP,
AlarmContact.isOptional(),
NO_EXTRA_PROPERTIES);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^" + Pattern.quote(assetEntity.getParentAsset().getIdentifier() + IDENTIFIER_SUFFIX) + "$");
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
ofNullable(entity.getParentAsset()).ifPresent(pa -> entity.setIdentifier(pa.getIdentifier() + IDENTIFIER_SUFFIX));
}
}
}

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 java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hs.validation.ArrayProperty.arrayOf;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsEMailAddressHostingAssetValidator extends HostingAssetEntityValidator {
private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names
private static final String EMAIL_ADDRESS_LOCAL_PART_REGEX = "[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+"; // RFC 5322
private static final String EMAIL_ADDRESS_DOMAIN_PART_REGEX = "[a-zA-Z0-9.-]+";
private static final String EMAIL_ADDRESS_FULL_REGEX = "^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "@" + EMAIL_ADDRESS_DOMAIN_PART_REGEX + "$";
public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322
HsEMailAddressHostingAssetValidator() {
super( HsHostingAssetType.EMAIL_ADDRESS,
AlarmContact.isOptional(),
stringProperty("local-part").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").required(),
stringProperty("sub-domain").matchesRegEx("^" + EMAIL_ADDRESS_LOCAL_PART_REGEX + "$").optional(),
arrayOf(
stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_FULL_REGEX)
).required().minLength(1));
}
@Override
public void preprocessEntity(final HsHostingAssetEntity entity) {
super.preprocessEntity(entity);
super.preprocessEntity(entity);
if (entity.getIdentifier() == null) {
entity.setIdentifier(combineIdentifier(entity));
}
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^"+ Pattern.quote(combineIdentifier(assetEntity)) + "$");
}
private static String combineIdentifier(final HsHostingAssetEntity emailAddressAssetEntity) {
return emailAddressAssetEntity.getDirectValue("local-part", String.class) +
ofNullable(emailAddressAssetEntity.getDirectValue("sub-domain", String.class)).map(s -> "." + s).orElse("") +
"@" +
emailAddressAssetEntity.getParentAsset().getIdentifier();
}
}

View File

@ -0,0 +1,31 @@
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.StringProperty.stringProperty;
class HsEMailAliasHostingAssetValidator extends HostingAssetEntityValidator {
private static final String UNIX_USER_REGEX = "^[a-z][a-z0-9]{2}[0-9]{2}(-[a-z0-9]+)?$"; // also accepts legacy pac-names
private static final String EMAIL_ADDRESS_REGEX = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; // RFC 5322
public static final int EMAIL_ADDRESS_MAX_LENGTH = 320; // according to RFC 5321 and RFC 5322
HsEMailAliasHostingAssetValidator() {
super( HsHostingAssetType.EMAIL_ALIAS,
AlarmContact.isOptional(),
arrayOf(
stringProperty("target").maxLength(EMAIL_ADDRESS_MAX_LENGTH).matchesRegEx(UNIX_USER_REGEX, EMAIL_ADDRESS_REGEX)
).required().minLength(1));
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$");
}
}

View File

@ -1,51 +0,0 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import lombok.experimental.UtilityClass;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import jakarta.validation.ValidationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.CLOUD_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
@UtilityClass
public class HsHostingAssetEntityValidators {
private static final Map<Enum<HsHostingAssetType>, HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType>> validators = new HashMap<>();
static {
register(CLOUD_SERVER, new HsCloudServerHostingAssetValidator());
register(MANAGED_SERVER, new HsManagedServerHostingAssetValidator());
register(MANAGED_WEBSPACE, new HsManagedWebspaceHostingAssetValidator());
}
private static void register(final Enum<HsHostingAssetType> type, final HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> validator) {
stream(validator.propertyValidators).forEach( entry -> {
entry.verifyConsistency(Map.entry(type, validator));
});
validators.put(type, validator);
}
public static HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> forType(final Enum<HsHostingAssetType> type) {
return validators.get(type);
}
public static Set<Enum<HsHostingAssetType>> types() {
return validators.keySet();
}
public static HsHostingAssetEntity valid(final HsHostingAssetEntity entityToSave) {
final var violations = HsHostingAssetEntityValidators.forType(entityToSave.getType()).validate(entityToSave);
if (!violations.isEmpty()) {
throw new ValidationException(violations.toString());
}
return entityToSave;
}
}

View File

@ -1,20 +1,60 @@
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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
import java.util.regex.Pattern;
class HsManagedServerHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_SERVER;
import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanProperty;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
class HsManagedServerHostingAssetValidator extends HostingAssetEntityValidator {
public HsManagedServerHostingAssetValidator() {
super(
integerProperty("CPUs").min(1).max(32).required(),
integerProperty("RAM").unit("GB").min(1).max(128).required(),
integerProperty("SSD").unit("GB").min(25).max(1000).step(25).required(),
integerProperty("HDD").unit("GB").min(0).max(4000).step(250).optional(),
integerProperty("Traffic").unit("GB").min(250).max(10000).step(250).required()
MANAGED_SERVER,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
// monitoring
integerProperty("monit_max_cpu_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ram_usage").unit("%").min(10).max(100).withDefault(92),
integerProperty("monit_max_ssd_usage").unit("%").min(10).max(100).withDefault(98),
integerProperty("monit_min_free_ssd").min(1).max(1000).withDefault(5),
integerProperty("monit_max_hdd_usage").unit("%").min(10).max(100).withDefault(95),
integerProperty("monit_min_free_hdd").min(1).max(4000).withDefault(10),
// other settings
// booleanProperty("fastcgi_small").withDefault(false), TODO.spec: clarify Salt-Grains
// database software
booleanProperty("software-pgsql").withDefault(true),
booleanProperty("software-mariadb").withDefault(true),
// PHP
enumerationProperty("php-default").valuesFromProperties("software-php-").withDefault("8.2"),
booleanProperty("software-php-5.6").withDefault(false),
booleanProperty("software-php-7.0").withDefault(false),
booleanProperty("software-php-7.1").withDefault(false),
booleanProperty("software-php-7.2").withDefault(false),
booleanProperty("software-php-7.3").withDefault(false),
booleanProperty("software-php-7.4").withDefault(true),
booleanProperty("software-php-8.0").withDefault(false),
booleanProperty("software-php-8.1").withDefault(false),
booleanProperty("software-php-8.2").withDefault(true),
// other software
booleanProperty("software-postfix-tls-1.0").withDefault(false),
booleanProperty("software-dovecot-tls-1.0").withDefault(false),
booleanProperty("software-clamav").withDefault(true),
booleanProperty("software-collabora").withDefault(false),
booleanProperty("software-libreoffice").withDefault(false),
booleanProperty("software-imagemagick-ghostscript").withDefault(false)
);
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
return Pattern.compile("^vm[0-9][0-9][0-9][0-9]$");
}
}

View File

@ -1,34 +1,25 @@
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 net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
import java.util.List;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.validation.IntegerPropertyValidator.integerProperty;
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.MANAGED_WEBSPACE;
class HsManagedWebspaceHostingAssetValidator extends HsEntityValidator<HsHostingAssetEntity, HsHostingAssetType> {
class HsManagedWebspaceHostingAssetValidator extends HostingAssetEntityValidator {
public HsManagedWebspaceHostingAssetValidator() {
super(
integerProperty("SSD").unit("GB").min(1).max(100).step(1).required(),
integerProperty("HDD").unit("GB").min(0).max(250).step(10).optional(),
integerProperty("Traffic").unit("GB").min(10).max(1000).step(10).required()
);
MANAGED_WEBSPACE,
AlarmContact.isOptional(), // hostmaster alert address is implicitly added
NO_EXTRA_PROPERTIES);
}
@Override
public List<String> validate(final HsHostingAssetEntity assetEntity) {
final var result = super.validate(assetEntity);
validateIdentifierPattern(result, assetEntity);
return result;
}
private static void validateIdentifierPattern(final List<String> result, final HsHostingAssetEntity assetEntity) {
final var expectedIdentifierPattern = "^" + assetEntity.getParentAsset().getBookingItem().getDebitor().getDefaultPrefix() + "[0-9][0-9]$";
if ( !assetEntity.getIdentifier().matches(expectedIdentifierPattern)) {
result.add("'identifier' expected to match '"+expectedIdentifierPattern+"', but is '" + assetEntity.getIdentifier() + "'");
}
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var prefixPattern =
!assetEntity.isLoaded()
? assetEntity.getParentAsset().getBookingItem().getProject().getDebitor().getDefaultPrefix()
: "[a-z][a-z0-9][a-z0-9]";
return Pattern.compile("^" + prefixPattern + "[0-9][0-9]$");
}
}

View File

@ -0,0 +1,48 @@
package net.hostsharing.hsadminng.hs.hosting.asset.validators;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
import java.util.regex.Pattern;
import static net.hostsharing.hsadminng.hs.validation.EnumerationProperty.enumerationProperty;
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
import static net.hostsharing.hsadminng.hs.validation.PasswordProperty.passwordProperty;
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
private static final int DASH_LENGTH = "-".length();
HsUnixUserHostingAssetValidator() {
super(
HsHostingAssetType.UNIX_USER,
AlarmContact.isOptional(),
integerProperty("SSD hard quota").unit("GB").maxFrom("SSD").optional(),
integerProperty("SSD soft quota").unit("GB").maxFrom("SSD hard quota").optional(),
integerProperty("HDD hard quota").unit("GB").maxFrom("HDD").optional(),
integerProperty("HDD soft quota").unit("GB").maxFrom("HDD hard quota").optional(),
enumerationProperty("shell")
.values("/bin/false", "/bin/bash", "/bin/csh", "/bin/dash", "/usr/bin/tcsh", "/usr/bin/zsh", "/usr/bin/passwd")
.withDefault("/bin/false"),
stringProperty("homedir").readOnly().computedBy(HsUnixUserHostingAssetValidator::computeHomedir),
stringProperty("totpKey").matchesRegEx("^0x([0-9A-Fa-f]{2})+$").minLength(20).maxLength(256).undisclosed().writeOnly().optional(),
passwordProperty("password").minLength(8).maxLength(40).hashedUsing(LinuxEtcShadowHashGenerator.Algorithm.SHA512).writeOnly());
}
@Override
protected Pattern identifierPattern(final HsHostingAssetEntity assetEntity) {
final var webspaceIdentifier = assetEntity.getParentAsset().getIdentifier();
return Pattern.compile("^"+webspaceIdentifier+"$|^"+webspaceIdentifier+"-[a-z0-9]+$");
}
private static String computeHomedir(final PropertiesProvider propertiesProvider) {
final var entity = (HsHostingAssetEntity) propertiesProvider;
final var webspaceName = entity.getParentAsset().getIdentifier();
return "/home/pacs/" + webspaceName
+ "/users/" + entity.getIdentifier().substring(webspaceName.length()+DASH_LENGTH);
}
}

View File

@ -0,0 +1,40 @@
### HsHostingAssetEntity-Validation
There is just a single `HsHostingAssetEntity` class for all types of hosting assets like Managed-Server, Managed-Webspace, Unix-Users, Databases etc. These are distinguished by `HsHostingAssetType HsHostingAssetEntity.type`.
For each of these types, a distinct validator has to be
implemented as a subclass of `HsHostingAssetEntityValidator` which needs to be registered (see `HsHostingAssetEntityValidatorRegistry`) for the relevant type(s).
### Kinds of Validations
#### Identifier validation
The identifier of a Hosting-Asset is for example the Webspace-Name like "xyz00" or a Unix-User-Name like "xyz00-test".
To validate the identifier, vverride the method `identifierPattern(...)` and return a regular expression to validate the identifier against. The regular expression can depend on the actual entity instance.
#### Reference validation
References in this context are:
- the related Booking-Item,
- the parent-Hosting-Asset,
- the Assigned-To-Hosting-Asset and
- the Contact.
The first parameters of the `HsHostingAssetEntityValidator` superclass take rule descriptors for these references. These are all Subclasses fo
### Validation Order
The validations are called in a sensible order. E.g. if a property value is not numeric, it makes no sense to check the total sum of such values to be within certain numeric values. And if the related booking item is of wrong type, it makes no sense to validate limits against sub-entities.
Properties are validated all at once, though. Thus, if multiple properties fail validation, all error messages are returned at once.
In general, the validation es executed in this order:
1. the entity itself
1. its references
2. its properties
2. the limits of the parent entity (parent asset + booking item)
3. limits against the own own-sub-entities
This implementation can be found in `HsHostingAssetEntityValidator.validate`.

View File

@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopAssetsApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
@ -13,14 +14,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.persistence.EntityNotFoundException;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopAssetsTransactionTypeResource.*;
@RestController
@ -97,9 +96,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
validateDebitTransaction(requestBody, violations);
validateCreditTransaction(requestBody, violations);
validateAssetValue(requestBody, violations);
if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]");
}
MultiValidationException.throwIfNotEmpty(violations);
}
private static void validateDebitTransaction(

View File

@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopSharesApi;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
import net.hostsharing.hsadminng.errors.MultiValidationException;
import net.hostsharing.hsadminng.mapper.Mapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
@ -14,14 +15,12 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import jakarta.validation.ValidationException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.BiConsumer;
import static java.lang.String.join;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.CANCELLATION;
import static net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionTypeResource.SUBSCRIPTION;
@ -99,9 +98,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
validateSubscriptionTransaction(requestBody, violations);
validateCancellationTransaction(requestBody, violations);
validateshareCount(requestBody, violations);
if (violations.size() > 0) {
throw new ValidationException("[" + join(", ", violations) + "]");
}
MultiValidationException.throwIfNotEmpty(violations);
}
private static void validateSubscriptionTransaction(

View File

@ -0,0 +1,63 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.Arrays;
import java.util.List;
import static java.util.Arrays.stream;
import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry;
@Setter
public class ArrayProperty<P extends ValidatableProperty<?, E>, E> extends ValidatableProperty<ArrayProperty<P, E>, E[]> {
private static final String[] KEY_ORDER =
insertNewEntriesAfterExistingEntry(
insertNewEntriesAfterExistingEntry(ValidatableProperty.KEY_ORDER, "required", "minLength" ,"maxLength"),
"propertyName", "elementsOf");
private final ValidatableProperty<?, E> elementsOf;
private Integer minLength;
private Integer maxLength;
private ArrayProperty(final ValidatableProperty<?, E> elementsOf) {
//noinspection unchecked
super((Class<E[]>) elementsOf.type.arrayType(), elementsOf.propertyName, KEY_ORDER);
this.elementsOf = elementsOf;
}
public static <T> ArrayProperty<?, T[]> arrayOf(final ValidatableProperty<?, T> elementsOf) {
//noinspection unchecked
return (ArrayProperty<?, T[]>) new ArrayProperty<>(elementsOf);
}
public ValidatableProperty<?, ?> minLength(final int minLength) {
this.minLength = minLength;
return self();
}
public ValidatableProperty<?, ?> maxLength(final int maxLength) {
this.maxLength = maxLength;
return self();
}
@Override
protected void validate(final List<String> result, final E[] propValue, final PropertiesProvider propProvider) {
if (minLength != null && propValue.length < minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length);
}
if (maxLength != null && propValue.length > maxLength) {
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length);
}
stream(propValue).forEach(e -> elementsOf.validate(result, e, propProvider));
}
@Override
protected String simpleTypeName() {
return elementsOf.simpleTypeName() + "[]";
}
@SafeVarargs
private String display(final E... propValue) {
return "[" + Arrays.toString(propValue) + "]";
}
}

View File

@ -0,0 +1,46 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanProperty extends ValidatableProperty<BooleanProperty, Boolean> {
private static final String[] KEY_ORDER = Array.join(ValidatableProperty.KEY_ORDER_HEAD, ValidatableProperty.KEY_ORDER_TAIL);
private Map.Entry<String, String> falseIf;
private BooleanProperty(final String propertyName) {
super(Boolean.class, propertyName, KEY_ORDER);
}
public static BooleanProperty booleanProperty(final String propertyName) {
return new BooleanProperty(propertyName);
}
public BooleanProperty falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final List<String> result, final Boolean propValue, final PropertiesProvider propProvider) {
if (falseIf != null && propValue) {
final Object referencedValue = propProvider.directProps().get(falseIf.getKey());
if (Objects.equals(referencedValue, falseIf.getValue())) {
result.add(propertyName + "' is expected to be false because " +
falseIf.getKey() + "=" + referencedValue + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -1,42 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.Objects;
@Setter
public class BooleanPropertyValidator extends HsPropertyValidator<Boolean> {
private Map.Entry<String, String> falseIf;
private BooleanPropertyValidator(final String propertyName) {
super(Boolean.class, propertyName);
}
public static BooleanPropertyValidator booleanProperty(final String propertyName) {
return new BooleanPropertyValidator(propertyName);
}
public HsPropertyValidator<Boolean> falseIf(final String refPropertyName, final String refPropertyValue) {
this.falseIf = new AbstractMap.SimpleImmutableEntry<>(refPropertyName, refPropertyValue);
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Boolean propValue, final Map<String, Object> props) {
if (falseIf != null && !Objects.equals(props.get(falseIf.getKey()), falseIf.getValue())) {
if (propValue) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be false because " +
propertiesName+"." + falseIf.getKey()+ "=" + falseIf.getValue() + " but is " + propValue);
}
}
}
@Override
protected String simpleTypeName() {
return "boolean";
}
}

View File

@ -0,0 +1,63 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.Arrays;
import java.util.List;
import static java.util.Arrays.stream;
@Setter
public class EnumerationProperty extends ValidatableProperty<EnumerationProperty, String> {
private static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD,
Array.of("values"),
ValidatableProperty.KEY_ORDER_TAIL);
private String[] values;
private EnumerationProperty(final String propertyName) {
super(String.class, propertyName, KEY_ORDER);
}
public static EnumerationProperty enumerationProperty(final String propertyName) {
return new EnumerationProperty(propertyName);
}
public EnumerationProperty values(final String... values) {
this.values = values;
return this;
}
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
if (hasDeferredInit()) {
if (this.values != null) {
throw new IllegalStateException("property " + this + " already has values");
}
this.values = doDeferredInit(allProperties);
}
}
public EnumerationProperty valuesFromProperties(final String propertyNamePrefix) {
this.setDeferredInit( (ValidatableProperty<?, ?>[] allProperties) -> stream(allProperties)
.map(ValidatableProperty::propertyName)
.filter(name -> name.startsWith(propertyNamePrefix))
.map(name -> name.substring(propertyNamePrefix.length()))
.toArray(String[]::new));
return this;
}
@Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
if (stream(values).noneMatch(v -> v.equals(propValue))) {
result.add(propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}

View File

@ -1,38 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
@Setter
public class EnumerationPropertyValidator extends HsPropertyValidator<String> {
private String[] values;
private EnumerationPropertyValidator(final String propertyName) {
super(String.class, propertyName);
}
public static EnumerationPropertyValidator enumerationProperty(final String propertyName) {
return new EnumerationPropertyValidator(propertyName);
}
public HsPropertyValidator<String> values(final String... values) {
this.values = values;
return this;
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final String propValue, final Map<String, Object> props) {
if (Arrays.stream(values).noneMatch(v -> v.equals(propValue))) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be one of " + Arrays.toString(values) + " but is '" + propValue + "'");
}
}
@Override
protected String simpleTypeName() {
return "enumeration";
}
}

View File

@ -1,49 +1,144 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
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;
public class HsEntityValidator<E extends Validatable<E, T>, T extends Enum<T>> {
// TODO.refa: rename to HsEntityProcessor, also subclasses
public abstract class HsEntityValidator<E extends PropertiesProvider> {
public final HsPropertyValidator<?>[] propertyValidators;
public final ValidatableProperty<?, ?>[] propertyValidators;
public HsEntityValidator(final HsPropertyValidator<?>... validators) {
public <T extends Enum <T>> HsEntityValidator(final ValidatableProperty<?, ?>... validators) {
propertyValidators = validators;
stream(propertyValidators).forEach(p -> p.deferredInit(propertyValidators));
}
public List<String> validate(final E assetEntity) {
final var result = new ArrayList<String>();
assetEntity.getProperties().keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add("'"+assetEntity.getPropertiesName()+"." + givenPropName + "' is not expected but is set to '" +assetEntity.getProperties().get(givenPropName) + "'");
}
});
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(assetEntity.getPropertiesName(), assetEntity.getProperties()));
});
return result;
}
public List<Map<String, Object>> properties() {
final var mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
return Arrays.stream(propertyValidators)
.map(propertyValidator -> propertyValidator.toMap(mapper))
.map(HsEntityValidator::asKeyValueMap)
protected static List<String> enrich(final String prefix, final List<String> messages) {
return messages.stream()
// TODO:refa: this is a bit hacky, I need to find the right place to add the prefix
.map(message -> message.startsWith("'") ? message : ("'" + prefix + "." + message))
.toList();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Map<String, Object> asKeyValueMap(final Map map) {
return (Map<String, Object>) map;
protected static String prefix(final String... parts) {
return String.join(".", parts);
}
public abstract List<String> validateEntity(final E entity);
public abstract List<String> validateContext(final E entity);
public final List<Map<String, Object>> properties() {
return Arrays.stream(propertyValidators)
.map(ValidatableProperty::toOrderedMap)
.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>();
// verify that all actually given properties are specified
final var properties = propsProvider.directProps();
properties.keySet().forEach( givenPropName -> {
if (stream(propertyValidators).map(pv -> pv.propertyName).noneMatch(propName -> propName.equals(givenPropName))) {
result.add(givenPropName + "' is not expected but is set to '" + properties.get(givenPropName) + "'");
}
});
// run all property validators
stream(propertyValidators).forEach(pv -> {
result.addAll(pv.validate(propsProvider));
});
return result;
}
@SafeVarargs
public static List<String> sequentiallyValidate(final Supplier<List<String>>... validators) {
return new ArrayList<>(stream(validators)
.map(Supplier::get)
.filter(violations -> !violations.isEmpty())
.findFirst()
.orElse(emptyList()));
}
protected static Integer getIntegerValueWithDefault0(final ValidatableProperty<?, ?> prop, final Map<String, Object> propValues) {
final var value = prop.getValue(propValues);
if (value instanceof Integer) {
return (Integer) value;
}
if (value == null) {
return 0;
}
throw new IllegalArgumentException(prop.propertyName + " Integer value expected, but got " + value);
}
protected static Integer toIntegerWithDefault0(final Object value) {
if (value instanceof Integer) {
return (Integer) value;
}
if (value == null) {
return 0;
}
throw new IllegalArgumentException("Integer value (or null) expected, but got " + value);
}
public void prepareProperties(final E entity) {
stream(propertyValidators).forEach(p -> {
if ( p.isWriteOnly() && p.isComputed()) {
entity.directProps().put(p.propertyName, p.compute(entity));
}
});
}
public Map<String, Object> revampProperties(final E entity, final Map<String, Object> config) {
final var copy = new HashMap<>(config);
stream(propertyValidators).forEach(p -> {
if (p.isWriteOnly()) {
copy.remove(p.propertyName);
} else if (p.isReadOnly() && p.isComputed()) {
copy.put(p.propertyName, p.compute(entity));
}
});
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

@ -1,67 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public abstract class HsPropertyValidator<T> {
final Class<T> type;
final String propertyName;
private Boolean required;
public static <K, V> Map.Entry<K, V> defType(K k, V v) {
return new SimpleImmutableEntry<>(k, v);
}
public HsPropertyValidator<T> required() {
required = Boolean.TRUE;
return this;
}
public HsPropertyValidator<T> optional() {
required = Boolean.FALSE;
return this;
}
public final List<String> validate(final String propertiesName, final Map<String, Object> props) {
final var result = new ArrayList<String>();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add("'"+propertiesName+"." + propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, propertiesName, (T) propValue, props);
} else {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be of type " + type + ", " +
"but is of type '" + propValue.getClass().getSimpleName() + "'");
}
}
return result;
}
protected abstract void validate(final ArrayList<String> result, final String propertiesName, final T propValue, final Map<String, Object> props);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
public Map<String, Object> toMap(final ObjectMapper mapper) {
final Map<String, Object> map = mapper.convertValue(this, Map.class);
map.put("type", simpleTypeName());
return map;
}
protected abstract String simpleTypeName();
}

View File

@ -0,0 +1,88 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import org.apache.commons.lang3.Validate;
import java.util.List;
@Setter
public class IntegerProperty extends ValidatableProperty<IntegerProperty, Integer> {
private final static String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD,
Array.of("unit", "min", "minFrom", "max", "maxFrom", "step"),
ValidatableProperty.KEY_ORDER_TAIL);
private String unit;
private Integer min;
private String minFrom;
private Integer max;
private String maxFrom;
private Integer step;
public static IntegerProperty integerProperty(final String propertyName) {
return new IntegerProperty(propertyName);
}
private IntegerProperty(final String propertyName) {
super(Integer.class, propertyName, KEY_ORDER);
}
@Override
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
Validate.isTrue(min == null || minFrom == null, "min and minFrom are exclusive, but both are given");
Validate.isTrue(max == null || maxFrom == null, "max and maxFrom are exclusive, but both are given");
}
public IntegerProperty minFrom(final String propertyName) {
minFrom = propertyName;
return this;
}
public IntegerProperty maxFrom(final String propertyName) {
maxFrom = propertyName;
return this;
}
@Override
public String unit() {
return unit;
}
public Integer max() {
return max;
}
@Override
protected void validate(final List<String> result, final Integer propValue, final PropertiesProvider propProvider) {
validateMin(result, propertyName, propValue, min);
validateMax(result, propertyName, propValue, max);
if (step != null && propValue % step != 0) {
result.add(propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
if (minFrom != null) {
validateMin(result, propertyName, propValue, propProvider.getContextValue(minFrom, Integer.class));
}
if (maxFrom != null) {
validateMax(result, propertyName, propValue, propProvider.getContextValue(maxFrom, Integer.class, 0));
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
private static void validateMin(final List<String> result, final String propertyName, final Integer propValue, final Integer min) {
if (min != null && propValue < min) {
result.add(propertyName + "' is expected to be at least " + min + " but is " + propValue);
}
}
private static void validateMax(final List<String> result, final String propertyName, final Integer propValue, final Integer max) {
if (max != null && propValue > max) {
result.add(propertyName + "' is expected to be at most " + max + " but is " + propValue);
}
}
}

View File

@ -1,42 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import java.util.ArrayList;
import java.util.Map;
@Setter
public class IntegerPropertyValidator extends HsPropertyValidator<Integer> {
private String unit;
private Integer min;
private Integer max;
private Integer step;
public static IntegerPropertyValidator integerProperty(final String propertyName) {
return new IntegerPropertyValidator(propertyName);
}
private IntegerPropertyValidator(final String propertyName) {
super(Integer.class, propertyName);
}
@Override
protected void validate(final ArrayList<String> result, final String propertiesName, final Integer propValue, final Map<String, Object> props) {
if (min != null && propValue < min) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be >= " + min + " but is " + propValue);
}
if (max != null && propValue > max) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be <= " + max + " but is " + propValue);
}
if (step != null && propValue % step != 0) {
result.add("'"+propertiesName+"." + propertyName + "' is expected to be multiple of " + step + " but is " + propValue);
}
}
@Override
protected String simpleTypeName() {
return "integer";
}
}

View File

@ -0,0 +1,80 @@
package net.hostsharing.hsadminng.hs.validation;
import net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm;
import lombok.Setter;
import java.util.List;
import java.util.stream.Stream;
import static java.util.Optional.ofNullable;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static net.hostsharing.hsadminng.mapper.Array.insertNewEntriesAfterExistingEntry;
@Setter
public class PasswordProperty extends StringProperty<PasswordProperty> {
private static final String[] KEY_ORDER = insertNewEntriesAfterExistingEntry(StringProperty.KEY_ORDER, "computed", "hashedUsing");
private Algorithm hashedUsing;
private PasswordProperty(final String propertyName) {
super(propertyName, KEY_ORDER);
undisclosed();
}
public static PasswordProperty passwordProperty(final String propertyName) {
return new PasswordProperty(propertyName);
}
@Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
super.validate(result, propValue, propProvider);
validatePassword(result, propValue);
}
public PasswordProperty hashedUsing(final Algorithm algorithm) {
this.hashedUsing = algorithm;
computedBy((entity)
-> ofNullable(entity.getDirectValue(propertyName, String.class))
.map(password -> hash(password).using(algorithm).withRandomSalt().generate())
.orElse(null));
return self();
}
@Override
protected String simpleTypeName() {
return "password";
}
private void validatePassword(final List<String> result, final String password) {
boolean hasLowerCase = false;
boolean hasUpperCase = false;
boolean hasDigit = false;
boolean hasSpecialChar = false;
boolean containsColon = false;
for (char c : password.toCharArray()) {
if (Character.isLowerCase(c)) {
hasLowerCase = true;
} else if (Character.isUpperCase(c)) {
hasUpperCase = true;
} else if (Character.isDigit(c)) {
hasDigit = true;
} else if (!Character.isLetterOrDigit(c)) {
hasSpecialChar = true;
}
if (c == ':') {
containsColon = true;
}
}
final long groupsCovered = Stream.of(hasLowerCase, hasUpperCase, hasDigit, hasSpecialChar).filter(v->v).count();
if ( groupsCovered < 3) {
result.add(propertyName + "' must contain at least one character of at least 3 of the following groups: upper case letters, lower case letters, digits, special characters");
}
if (containsColon) {
result.add(propertyName + "' must not contain colon (':')");
}
}
}

View File

@ -0,0 +1,31 @@
package net.hostsharing.hsadminng.hs.validation;
import java.util.Map;
public interface PropertiesProvider {
Map<String, Object> directProps();
Object getContextValue(final String propName);
default <T> T getDirectValue(final String propName, final Class<T> clazz) {
return cast(propName, directProps().get(propName), clazz, null);
}
default <T> T getContextValue(final String propName, final Class<T> clazz) {
return cast(propName, getContextValue(propName), clazz, null);
}
default <T> T getContextValue(final String propName, final Class<T> clazz, final T defaultValue) {
return cast(propName, getContextValue(propName), clazz, defaultValue);
}
private static <T> T cast( final String propName, final Object value, final Class<T> clazz, final T defaultValue) {
if (value == null && defaultValue != null) {
return defaultValue;
}
if (value == null || clazz.isInstance(value)) {
return clazz.cast(value);
}
throw new IllegalStateException(propName + " expected to be an "+clazz.getSimpleName()+", but got '" + value + "'");
}
}

View File

@ -0,0 +1,95 @@
package net.hostsharing.hsadminng.hs.validation;
import lombok.Setter;
import net.hostsharing.hsadminng.mapper.Array;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.util.Arrays.stream;
@Setter
public class StringProperty<P extends StringProperty<P>> extends ValidatableProperty<P, String> {
protected static final String[] KEY_ORDER = Array.join(
ValidatableProperty.KEY_ORDER_HEAD,
Array.of("matchesRegEx", "minLength", "maxLength", "provided"),
ValidatableProperty.KEY_ORDER_TAIL,
Array.of("undisclosed"));
private String[] provided;
private Pattern[] matchesRegEx;
private Integer minLength;
private Integer maxLength;
private boolean undisclosed;
protected StringProperty(final String propertyName) {
super(String.class, propertyName, KEY_ORDER);
}
protected StringProperty(final String propertyName, final String[] keyOrder) {
super(String.class, propertyName, keyOrder);
}
public static StringProperty<?> stringProperty(final String propertyName) {
return new StringProperty<>(propertyName);
}
public P minLength(final int minLength) {
this.minLength = minLength;
return self();
}
public P maxLength(final int maxLength) {
this.maxLength = maxLength;
return self();
}
public P matchesRegEx(final String... regExPattern) {
this.matchesRegEx = stream(regExPattern).map(Pattern::compile).toArray(Pattern[]::new);
return self();
}
/// predifined values, similar to fixed values in a combobox
public P provided(final String... provided) {
this.provided = provided;
return self();
}
/**
* The property value is not disclosed in error messages.
*
* @return this;
*/
public P undisclosed() {
this.undisclosed = true;
return self();
}
@Override
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
if (minLength != null && propValue.length()<minLength) {
result.add(propertyName + "' length is expected to be at min " + minLength + " but length of " + display(propValue) + " is " + propValue.length());
}
if (maxLength != null && propValue.length()>maxLength) {
result.add(propertyName + "' length is expected to be at max " + maxLength + " but length of " + display(propValue) + " is " + propValue.length());
}
if (matchesRegEx != null &&
stream(matchesRegEx).map(p -> p.matcher(propValue)).noneMatch(Matcher::matches)) {
result.add(propertyName + "' is expected to match any of " + Arrays.toString(matchesRegEx) + " but " + display(propValue) + " does not match" + (matchesRegEx.length>1?" any":""));
}
if (isReadOnly() && propValue != null) {
result.add(propertyName + "' is readonly but given as " + display(propValue));
}
}
private String display(final String propValue) {
return undisclosed ? "provided value" : ("'" + propValue + "'");
}
@Override
protected String simpleTypeName() {
return "string";
}
}

View File

@ -1,13 +0,0 @@
package net.hostsharing.hsadminng.hs.validation;
import java.util.Map;
public interface Validatable<E, T extends Enum<T>> {
Enum<T> getType();
String getPropertiesName();
Map<String, Object> getProperties();
}

View File

@ -0,0 +1,275 @@
package net.hostsharing.hsadminng.hs.validation;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.experimental.Accessors;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.mapper.Array;
import org.apache.commons.lang3.function.TriFunction;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.util.Collections.emptyList;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang3.ObjectUtils.isArray;
@Getter
@RequiredArgsConstructor
public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T> {
protected static final String[] KEY_ORDER_HEAD = Array.of("propertyName");
protected static final String[] KEY_ORDER_TAIL = Array.of("required", "defaultValue", "readOnly", "writeOnly", "computed", "isTotalsValidator", "thresholdPercentage");
protected static final String[] KEY_ORDER = Array.join(KEY_ORDER_HEAD, KEY_ORDER_TAIL);
final Class<T> type;
final String propertyName;
@JsonIgnore
private final String[] keyOrder;
private Boolean required;
private T defaultValue;
@JsonIgnore
private Function<PropertiesProvider, T> computedBy;
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean computed; // used in descriptor, because computedBy cannot be rendered to a text string
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean readOnly;
@Accessors(makeFinal = true, chain = true, fluent = false)
private boolean writeOnly;
private Function<ValidatableProperty<?, ?>[], T[]> deferredInit;
private boolean isTotalsValidator = false;
@JsonIgnore
private List<Function<HsBookingItemEntity, List<String>>> asTotalLimitValidators; // TODO.impl: move to BookingItemIntegerProperty
private Integer thresholdPercentage; // TODO.impl: move to IntegerProperty
public final P self() {
//noinspection unchecked
return (P) this;
}
public String unit() {
return null;
}
protected void setDeferredInit(final Function<ValidatableProperty<?, ?>[], T[]> function) {
this.deferredInit = function;
}
public boolean hasDeferredInit() {
return deferredInit != null;
}
public T[] doDeferredInit(final ValidatableProperty<?, ?>[] allProperties) {
return deferredInit.apply(allProperties);
}
public P writeOnly() {
this.writeOnly = true;
optional();
return self();
}
public P readOnly() {
this.readOnly = true;
optional();
return self();
}
public P required() {
required = TRUE;
return self();
}
public ValidatableProperty<P, T> optional() {
required = FALSE;
return this;
}
public P withDefault(final T value) {
defaultValue = value;
required = FALSE;
return self();
}
public void deferredInit(final ValidatableProperty<?, ?>[] allProperties) {
}
public P asTotalLimit() {
isTotalsValidator = true;
return self();
}
public P asTotalLimitFor(final String propertyName, final String propertyValue) {
if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>();
}
final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator =
(final HsBookingItemEntity entity, final IntegerProperty prop, final Integer factor) -> {
final var total = entity.getSubBookingItems().stream()
.map(server -> server.getResources().get(propertyName))
.filter(propertyValue::equals)
.count();
final long limitingValue = ofNullable(prop.getValue(entity.getResources())).orElse(0);
if (total > factor*limitingValue) {
return List.of(
prop.propertyName() + " maximum total is " + (factor*limitingValue) + ", but actual total for " + propertyName + "=" + propertyValue + " is " + total
);
}
return emptyList();
};
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, 1));
return self();
}
public String propertyName() {
return propertyName;
}
public boolean isTotalsValidator() {
return isTotalsValidator || asTotalLimitValidators != null;
}
public Integer thresholdPercentage() {
return thresholdPercentage;
}
public ValidatableProperty<P, T> eachComprising(final int factor, final TriFunction<HsBookingItemEntity, IntegerProperty, Integer, List<String>> validator) {
if (asTotalLimitValidators == null) {
asTotalLimitValidators = new ArrayList<>();
}
asTotalLimitValidators.add((final HsBookingItemEntity entity) -> validator.apply(entity, (IntegerProperty)this, factor));
return this;
}
public P withThreshold(final Integer percentage) {
this.thresholdPercentage = percentage;
return self();
}
public final List<String> validate(final PropertiesProvider propsProvider) {
final var result = new ArrayList<String>();
final var props = propsProvider.directProps();
final var propValue = props.get(propertyName);
if (propValue == null) {
if (required) {
result.add(propertyName + "' is required but missing");
}
}
if (propValue != null){
if ( type.isInstance(propValue)) {
//noinspection unchecked
validate(result, (T) propValue, propsProvider);
} else {
result.add(propertyName + "' is expected to be of type " + type.getSimpleName() + ", " +
"but is of type " + propValue.getClass().getSimpleName() + "");
}
}
return result;
}
protected abstract void validate(final List<String> result, final T propValue, final PropertiesProvider propProvider);
public void verifyConsistency(final Map.Entry<? extends Enum<?>, ?> typeDef) {
if (required == null ) {
throw new IllegalStateException(typeDef.getKey() + "[" + propertyName + "] not fully initialized, please call either .required() or .optional()" );
}
}
@SuppressWarnings("unchecked")
public T getValue(final Map<String, Object> propValues) {
return (T) Optional.ofNullable(propValues.get(propertyName)).orElse(defaultValue);
}
protected abstract String simpleTypeName();
public Map<String, Object> toOrderedMap() {
Map<String, Object> sortedMap = new LinkedHashMap<>();
sortedMap.put("type", simpleTypeName());
// Add entries according to the given order
for (String key : keyOrder) {
final Optional<Object> propValue = getPropertyValue(key);
propValue.filter(ValidatableProperty::isToBeRendered).ifPresent(o -> sortedMap.put(key, o));
}
return sortedMap;
}
private static boolean isToBeRendered(final Object v) {
return !(v instanceof Boolean b) || b;
}
@SneakyThrows
private Optional<Object> getPropertyValue(final String key) {
return getPropertyValue(getClass(), key);
}
@SneakyThrows
private Optional<Object> getPropertyValue(final Class<?> clazz, final String key) {
try {
final var field = clazz.getDeclaredField(key);
field.setAccessible(true);
return Optional.ofNullable(arrayToList(field.get(this)));
} catch (final NoSuchFieldException exc) {
if (clazz.getSuperclass() != null) {
return getPropertyValue(clazz.getSuperclass(), key);
}
throw exc;
}
}
private Object arrayToList(final Object value) {
if (isArray(value)) {
return Arrays.stream((Object[])value).map(Object::toString).toList();
}
return value;
}
public List<String> validateTotals(final HsBookingItemEntity bookingItem) {
if (asTotalLimitValidators==null) {
return emptyList();
}
return asTotalLimitValidators.stream()
.map(v -> v.apply(bookingItem))
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.toList();
}
public P computedBy(final Function<PropertiesProvider, T> compute) {
this.computedBy = compute;
this.computed = true;
return self();
}
public <E extends PropertiesProvider> T compute(final E entity) {
return computedBy.apply(entity);
}
@Override
public String toString() {
return toOrderedMap().toString();
}
}

View File

@ -1,10 +1,13 @@
package net.hostsharing.hsadminng.rbac.test;
package net.hostsharing.hsadminng.mapper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import static java.util.Arrays.asList;
/**
* Java has List.of(...), Set.of(...) and Map.of(...) all with varargs parameter,
* but no Array.of(...). Here it is.
@ -37,4 +40,30 @@ public class Array {
return resultList.toArray(String[]::new);
}
public static String[] join(final String[]... parts) {
final String[] joined = Arrays.stream(parts)
.flatMap(Arrays::stream)
.toArray(String[]::new);
return joined;
}
public static <T> T[] emptyArray() {
return of();
}
@SafeVarargs
public static <T> T[] insertNewEntriesAfterExistingEntry(final T[] array, final T entryToFind, final T... newEntries) {
final var arrayList = new ArrayList<>(asList(array));
final var index = arrayList.indexOf(entryToFind);
if (index < 0) {
throw new IllegalArgumentException("entry "+ entryToFind + " not found in " + Arrays.toString(array));
}
for (int n = 0; n < newEntries.length; ++n) {
arrayList.add(index +n + 1, newEntries[n]);
}
@SuppressWarnings("unchecked")
final var extendedArray = (T[]) java.lang.reflect.Array.newInstance(array.getClass().getComponentType(), array.length);
return arrayList.toArray(extendedArray);
}
}

View File

@ -53,13 +53,20 @@ public class PatchableMapWrapper<T> implements Map<String, T> {
}
public String toString() {
return "{ "
return "{\n"
+ (
keySet().stream().sorted()
.map(k -> k + ": " + get(k)))
.collect(joining(", ")
.map(k -> " \"" + k + "\": " + optionallyQuoted(get(k))))
.collect(joining(",\n")
)
+ " }";
+ "\n}\n";
}
private Object optionallyQuoted(final Object value) {
if ( value instanceof Number || value instanceof Boolean ) {
return value;
}
return "\"" + value + "\"";
}
// --- below just delegating methods --------------------------------

View File

@ -150,7 +150,7 @@ public class InsertTriggerGenerator {
returns trigger
language plpgsql as $$
begin
raise exception '[403] insert into ${rawSubTable} not allowed regardless of current subject, no insert permissions grated at all';
raise exception '[403] insert into ${rawSubTable} values(%) not allowed regardless of current subject, no insert permissions granted at all', NEW;
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg
@ -254,8 +254,8 @@ public class InsertTriggerGenerator {
private void generateInsertPermissionsChecksFooter(final StringWriter plPgSql) {
plPgSql.writeLn();
plPgSql.writeLn("""
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
raise exception '[403] insert into ${rawSubTable} values(%) not allowed for current subjects % (%)',
NEW, currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger ${rawSubTable}_insert_permission_check_tg

View File

@ -62,6 +62,8 @@ public class RbacGrantsDiagramService {
@PersistenceContext
private EntityManager em;
private Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>();
public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
@ -102,7 +104,7 @@ public class RbacGrantsDiagramService {
}
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
final var grants = findDescendantsByUuid(refUuid);
grants.forEach(g -> {
if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) {
return;
@ -114,6 +116,11 @@ public class RbacGrantsDiagramService {
});
}
private List<RawRbacGrantEntity> findDescendantsByUuid(final UUID refUuid) {
// TODO.impl: if that UUID already got processed, do we need to return anything at all?
return descendantsByUuid.computeIfAbsent(refUuid, uuid -> rawGrantRepo.findByDescendantUuid(uuid));
}
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
final var entities =
includes.contains(DETAILS)

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

View File

@ -13,5 +13,7 @@ map:
- type: string:uuid => java.util.UUID
paths:
/api/hs/booking/projects/{bookingProjectUuid}:
null: org.openapitools.jackson.nullable.JsonNullable
/api/hs/booking/items/{bookingItemUuid}:
null: org.openapitools.jackson.nullable.JsonNullable

View File

@ -51,7 +51,7 @@ components:
HsBookingItemInsert:
type: object
properties:
debitorUuid:
projectUuid:
type: string
format: uuid
nullable: false
@ -62,10 +62,6 @@ components:
minLength: 3
maxLength: 80
nullable: false
validFrom:
type: string
format: date
nullable: false
validTo:
type: string
format: date
@ -74,7 +70,7 @@ components:
$ref: '#/components/schemas/BookingResources'
required:
- caption
- debitorUuid
- projectUuid
- validFrom
- resources
additionalProperties: false

View File

@ -1,19 +1,19 @@
get:
summary: Returns a list of all booking items for a specified debitor.
description: Returns the list of all booking items for a specified debitor which are visible to the current user or any of it's assumed roles.
summary: Returns a list of all booking items for a specified project.
description: Returns the list of all booking items for a specified project which are visible to the current user or any of it's assumed roles.
tags:
- hs-booking-items
operationId: listBookingItemsByDebitorUuid
operationId: listBookingItemsByProjectUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUuid
- name: projectUuid
in: query
required: true
schema:
type: string
format: uuid
description: The UUID of the debitor, whose booking items are to be listed.
description: The UUID of the project, whose booking items are to be listed.
responses:
"200":
description: OK

View File

@ -0,0 +1,40 @@
components:
schemas:
HsBookingProject:
type: object
properties:
uuid:
type: string
format: uuid
caption:
type: string
required:
- uuid
- caption
HsBookingProjectPatch:
type: object
properties:
caption:
type: string
nullable: true
HsBookingProjectInsert:
type: object
properties:
debitorUuid:
type: string
format: uuid
nullable: false
caption:
type: string
minLength: 3
maxLength: 80
nullable: false
required:
- debitorUuid
- caption
additionalProperties: false

View File

@ -0,0 +1,83 @@
get:
tags:
- hs-booking-projects
description: 'Fetch a single booking project its uuid, if visible for the current subject.'
operationId: getBookingProjectByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the booking project to fetch.
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
patch:
tags:
- hs-booking-projects
description: 'Updates a single booking project identified by its uuid, if permitted for the current subject.'
operationId: patchBookingProject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path
required: true
schema:
type: string
format: uuid
requestBody:
content:
'application/json':
schema:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProjectPatch'
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
delete:
tags:
- hs-booking-projects
description: 'Delete a single booking project identified by its uuid, if permitted for the current subject.'
operationId: deleteBookingIemByUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: bookingProjectUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the booking project to delete.
responses:
"204":
description: No Content
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
"404":
$ref: 'error-responses.yaml#/components/responses/NotFound'

View File

@ -0,0 +1,58 @@
get:
summary: Returns a list of all booking projects for a specified debitor.
description: Returns the list of all booking projects for a specified debitor which are visible to the current user or any of it's assumed roles.
tags:
- hs-booking-projects
operationId: listBookingProjectsByDebitorUuid
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUuid
in: query
required: true
schema:
type: string
format: uuid
description: The UUID of the debitor, whose booking projects are to be listed.
responses:
"200":
description: OK
content:
'application/json':
schema:
type: array
items:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
post:
summary: Adds a new project as a container for booking items.
tags:
- hs-booking-projects
operationId: addBookingProject
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
requestBody:
description: A JSON object describing the new booking project.
required: true
content:
application/json:
schema:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProjectInsert'
responses:
"201":
description: Created
content:
'application/json':
schema:
$ref: 'hs-booking-project-schemas.yaml#/components/schemas/HsBookingProject'
"401":
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: 'error-responses.yaml#/components/responses/Forbidden'
"409":
$ref: 'error-responses.yaml#/components/responses/Conflict'

View File

@ -8,6 +8,15 @@ servers:
paths:
# Projects
/api/hs/booking/projects:
$ref: "hs-booking-projects.yaml"
/api/hs/booking/projects/{bookingProjectUuid}:
$ref: "hs-booking-projects-with-uuid.yaml"
# Items
/api/hs/booking/items:

View File

@ -11,6 +11,10 @@ components:
- MANAGED_WEBSPACE
- UNIX_USER
- DOMAIN_SETUP
- DOMAIN_DNS_SETUP
- DOMAIN_HTTP_SETUP
- DOMAIN_SMTP_SETUP
- DOMAIN_MBOX_SETUP
- EMAIL_ALIAS
- EMAIL_ADDRESS
- PGSQL_USER
@ -30,6 +34,8 @@ components:
type: string
caption:
type: string
alarmContact:
$ref: '../hs-office/hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
config:
$ref: '#/components/schemas/HsHostingAssetConfiguration'
required:
@ -44,6 +50,10 @@ components:
caption:
type: string
nullable: true
alarmContactUuid:
type: string
format: uuid
nullable: true
config:
$ref: '#/components/schemas/HsHostingAssetConfiguration'
@ -70,6 +80,10 @@ components:
minLength: 3
maxLength: 80
nullable: false
alarmContactUuid:
type: string
format: uuid
nullable: true
config:
$ref: '#/components/schemas/HsHostingAssetConfiguration'
required:

View File

@ -7,13 +7,13 @@ get:
parameters:
- $ref: 'auth.yaml#/components/parameters/currentUser'
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
- name: debitorUuid
- name: projectUuid
in: query
required: false
schema:
type: string
format: uuid
description: The UUID of the debitor, whose hosting assets are to be listed.
description: The UUID of the project, whose hosting assets are to be listed.
- name: parentAssetUuid
in: query
required: false

View File

@ -149,7 +149,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar)
declare
cleanIdentifier varchar;
begin
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._]+', '', 'g');
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._|]+', '', 'g');
return cleanIdentifier;
end; $$;

View File

@ -118,10 +118,13 @@ begin
sql = format($sql$
create or replace function %1$sUuidByIdName(givenIdName varchar)
returns uuid
language sql
strict as $f$
select uuid from %1$s_iv iv where iv.idName = givenIdName;
$f$;
language plpgsql as $f$
declare
singleMatch uuid;
begin
select uuid into strict singleMatch from %1$s_iv iv where iv.idName = givenIdName;
return singleMatch;
end; $f$;
$sql$, targetTable);
execute sql;

View File

@ -0,0 +1,17 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-booking-debitor-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create view hs_booking_debitor_rv as
select debitor.uuid,
debitor.version,
(partner.partnerNumber::varchar || debitor.debitorNumberSuffix)::numeric as debitorNumber,
debitor.defaultPrefix
from hs_office_debitor_rv debitor
-- RBAC for debitor is sufficient, for faster access we are bypassing RBAC for the join tables
join hs_office_relation debitorRel on debitor.debitorReluUid=debitorRel.uuid
join hs_office_relation partnerRel on partnerRel.holderUuid=debitorRel.anchorUuid
join hs_office_partner partner on partner.partnerReluUid=partnerRel.uuid;
--//

View File

@ -0,0 +1,22 @@
--liquibase formatted sql
-- ============================================================================
--changeset booking-project-MAIN-TABLE:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create table if not exists hs_booking_project
(
uuid uuid unique references RbacObject (uuid),
version int not null default 0,
debitorUuid uuid not null references hs_office_debitor(uuid),
caption varchar(80) not null
);
--//
-- ============================================================================
--changeset hs-booking-project-MAIN-TABLE-JOURNAL:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call create_journal('hs_booking_project');
--//

View File

@ -0,0 +1,63 @@
### rbac project
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph debitorRel["`**debitorRel**`"]
direction TB
style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph debitorRel:roles[ ]
style debitorRel:roles fill:#99bcdb,stroke:white
role:debitorRel:OWNER[[debitorRel:OWNER]]
role:debitorRel:ADMIN[[debitorRel:ADMIN]]
role:debitorRel:AGENT[[debitorRel:AGENT]]
role:debitorRel:TENANT[[debitorRel:TENANT]]
end
end
subgraph project["`**project**`"]
direction TB
style project fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph project:roles[ ]
style project:roles fill:#dd4901,stroke:white
role:project:OWNER[[project:OWNER]]
role:project:ADMIN[[project:ADMIN]]
role:project:AGENT[[project:AGENT]]
role:project:TENANT[[project:TENANT]]
end
subgraph project:permissions[ ]
style project:permissions fill:#dd4901,stroke:white
perm:project:INSERT{{project:INSERT}}
perm:project:DELETE{{project:DELETE}}
perm:project:UPDATE{{project:UPDATE}}
perm:project:SELECT{{project:SELECT}}
end
end
%% granting roles to roles
role:global:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
role:debitorRel:AGENT -.-> role:debitorRel:TENANT
role:debitorRel:AGENT ==> role:project:OWNER
role:project:OWNER ==> role:project:ADMIN
role:project:ADMIN ==> role:project:AGENT
role:project:AGENT ==> role:project:TENANT
role:project:TENANT ==> role:debitorRel:TENANT
%% granting permissions to roles
role:debitorRel:ADMIN ==> perm:project:INSERT
role:global:ADMIN ==> perm:project:DELETE
role:project:ADMIN ==> perm:project:UPDATE
role:project:TENANT ==> perm:project:SELECT
```

View File

@ -3,29 +3,29 @@
-- ============================================================================
--changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--//
--changeset hs-booking-project-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('hs_booking_item');
call generateRelatedRbacObject('hs_booking_project');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
--changeset hs-booking-project-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item');
call generateRbacRoleDescriptors('hsBookingProject', 'hs_booking_project');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--//
--changeset hs-booking-project-rbac-insert-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
*/
create or replace procedure buildRbacSystemForHsBookingItem(
NEW hs_booking_item
create or replace procedure buildRbacSystemForHsBookingProject(
NEW hs_booking_project
)
language plpgsql as $$
@ -48,27 +48,25 @@ begin
perform createRoleWithGrants(
hsBookingItemOWNER(NEW),
hsBookingProjectOWNER(NEW),
incomingSuperRoles => array[hsOfficeRelationAGENT(newDebitorRel)]
);
perform createRoleWithGrants(
hsBookingItemADMIN(NEW),
hsBookingProjectADMIN(NEW),
permissions => array['UPDATE'],
incomingSuperRoles => array[
hsBookingItemOWNER(NEW),
hsOfficeRelationAGENT(newDebitorRel)]
incomingSuperRoles => array[hsBookingProjectOWNER(NEW)]
);
perform createRoleWithGrants(
hsBookingItemAGENT(NEW),
incomingSuperRoles => array[hsBookingItemADMIN(NEW)]
hsBookingProjectAGENT(NEW),
incomingSuperRoles => array[hsBookingProjectADMIN(NEW)]
);
perform createRoleWithGrants(
hsBookingItemTENANT(NEW),
hsBookingProjectTENANT(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[hsBookingItemAGENT(NEW)],
incomingSuperRoles => array[hsBookingProjectAGENT(NEW)],
outgoingSubRoles => array[hsOfficeRelationTENANT(newDebitorRel)]
);
@ -78,81 +76,81 @@ begin
end; $$;
/*
AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row.
AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_project row.
*/
create or replace function insertTriggerForHsBookingItem_tf()
create or replace function insertTriggerForHsBookingProject_tf()
returns trigger
language plpgsql
strict as $$
begin
call buildRbacSystemForHsBookingItem(NEW);
call buildRbacSystemForHsBookingProject(NEW);
return NEW;
end; $$;
create trigger insertTriggerForHsBookingItem_tg
after insert on hs_booking_item
create trigger insertTriggerForHsBookingProject_tg
after insert on hs_booking_project
for each row
execute procedure insertTriggerForHsBookingItem_tf();
execute procedure insertTriggerForHsBookingProject_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
--changeset hs-booking-project-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to hs_office_relation ----------------------------
/*
Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_office_relation rows.
Grants INSERT INTO hs_booking_project permissions to specified role of pre-existing hs_office_relation rows.
*/
do language plpgsql $$
declare
row hs_office_relation;
begin
call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_office_relation rows');
call defineContext('create INSERT INTO hs_booking_project permissions for pre-exising hs_office_relation rows');
FOR row IN SELECT * FROM hs_office_relation
WHERE type = 'DEBITOR'
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
createPermission(row.uuid, 'INSERT', 'hs_booking_project'),
hsOfficeRelationADMIN(row));
END LOOP;
end;
$$;
/**
Grants hs_booking_item INSERT permission to specified role of new hs_office_relation rows.
Grants hs_booking_project INSERT permission to specified role of new hs_office_relation rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_hs_office_relation_tf()
create or replace function new_hs_booking_project_grants_insert_to_hs_office_relation_tf()
returns trigger
language plpgsql
strict as $$
begin
if NEW.type = 'DEBITOR' then
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
createPermission(NEW.uuid, 'INSERT', 'hs_booking_project'),
hsOfficeRelationADMIN(NEW));
end if;
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_hs_office_relation_tg
create trigger z_new_hs_booking_project_grants_insert_to_hs_office_relation_tg
after insert on hs_office_relation
for each row
execute procedure new_hs_booking_item_grants_insert_to_hs_office_relation_tf();
execute procedure new_hs_booking_project_grants_insert_to_hs_office_relation_tf();
-- ============================================================================
--changeset hs_booking_item-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
--changeset hs_booking_project-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_item.
Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_project.
*/
create or replace function hs_booking_item_insert_permission_check_tf()
create or replace function hs_booking_project_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
@ -164,47 +162,45 @@ begin
JOIN hs_office_debitor debitor ON debitor.debitorRelUuid = debitorRel.uuid
WHERE debitor.uuid = NEW.debitorUuid
);
assert superObjectUuid is not null, 'object uuid fetched depending on hs_booking_item.debitorUuid must not be null, also check fetchSql in RBAC DSL';
if hasInsertPermission(superObjectUuid, 'hs_booking_item') then
assert superObjectUuid is not null, 'object uuid fetched depending on hs_booking_project.debitorUuid must not be null, also check fetchSql in RBAC DSL';
if hasInsertPermission(superObjectUuid, 'hs_booking_project') then
return NEW;
end if;
raise exception '[403] insert into hs_booking_item not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
raise exception '[403] insert into hs_booking_project values(%) not allowed for current subjects % (%)',
NEW, currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger hs_booking_item_insert_permission_check_tg
before insert on hs_booking_item
create trigger hs_booking_project_insert_permission_check_tg
before insert on hs_booking_project
for each row
execute procedure hs_booking_item_insert_permission_check_tf();
execute procedure hs_booking_project_insert_permission_check_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--//
--changeset hs-booking-project-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacIdentityViewFromQuery('hs_booking_item',
call generateRbacIdentityViewFromQuery('hs_booking_project',
$idName$
SELECT bookingItem.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingItem.caption) as idName
FROM hs_booking_item bookingItem
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingItem.debitorUuid
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || cleanIdentifier(bookingProject.caption) as idName
FROM hs_booking_project bookingProject
JOIN hs_office_debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
$idName$);
--//
-- ============================================================================
--changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
--changeset hs-booking-project-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_booking_item',
call generateRbacRestrictedView('hs_booking_project',
$orderBy$
validity
caption
$orderBy$,
$updates$
version = new.version,
caption = new.caption,
validity = new.validity,
resources = new.resources
caption = new.caption
$updates$);
--//

View File

@ -2,13 +2,13 @@
-- ============================================================================
--changeset hs-booking-item-TEST-DATA-GENERATOR:1 endDelimiter:--//
--changeset hs-booking-project-TEST-DATA-GENERATOR:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a single hs_booking_item test record.
Creates a single hs_booking_project test record.
*/
create or replace procedure createHsBookingItemTransactionTestData(
create or replace procedure createHsBookingProjectTransactionTestData(
givenPartnerNumber numeric,
givenDebitorSuffix char(2)
)
@ -17,7 +17,7 @@ declare
currentTask varchar;
relatedDebitor hs_office_debitor;
begin
currentTask := 'creating booking-item test-data ' || givenPartnerNumber::text || givenDebitorSuffix;
currentTask := 'creating booking-project test-data ' || givenPartnerNumber::text || givenDebitorSuffix;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
execute format('set local hsadminng.currentTask to %L', currentTask);
@ -28,26 +28,24 @@ begin
join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid
where partner.partnerNumber = givenPartnerNumber and debitor.debitorNumberSuffix = givenDebitorSuffix;
raise notice 'creating test booking-item: %', givenPartnerNumber::text || givenDebitorSuffix::text;
raise notice 'creating test booking-project: %', givenDebitorSuffix::text;
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert
into hs_booking_item (uuid, debitoruuid, type, caption, validity, resources)
values (uuid_generate_v4(), relatedDebitor.uuid, 'MANAGED_SERVER', 'some ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SDD": 512, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'CLOUD_SERVER', 'some CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "HDD": 1024, "Traffic": 42 }'::jsonb),
(uuid_generate_v4(), relatedDebitor.uuid, 'PRIVATE_CLOUD', 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "SDD": 10240, "HDD": 10240, "Traffic": 42 }'::jsonb);
into hs_booking_project (uuid, debitoruuid, caption)
values (uuid_generate_v4(), relatedDebitor.uuid, 'D-' || givenPartnerNumber::text || givenDebitorSuffix || ' default project');
end; $$;
--//
-- ============================================================================
--changeset hs-booking-item-TEST-DATA-GENERATION:1 context=dev,tc endDelimiter:--//
--changeset hs-booking-project-TEST-DATA-GENERATION:1 context=dev,tc endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call createHsBookingItemTransactionTestData(10001, '11');
call createHsBookingItemTransactionTestData(10002, '12');
call createHsBookingItemTransactionTestData(10003, '13');
call createHsBookingProjectTransactionTestData(10001, '11');
call createHsBookingProjectTransactionTestData(10002, '12');
call createHsBookingProjectTransactionTestData(10003, '13');
end;
$$;
--//

View File

@ -17,11 +17,15 @@ create table if not exists hs_booking_item
(
uuid uuid unique references RbacObject (uuid),
version int not null default 0,
debitorUuid uuid not null references hs_office_debitor(uuid),
projectUuid uuid null references hs_booking_project(uuid),
type HsBookingItemType not null,
parentItemUuid uuid null references hs_booking_item(uuid) initially deferred,
validity daterange not null,
caption varchar(80) not null,
resources jsonb not null
resources jsonb not null,
constraint chk_hs_booking_item_has_project_or_parent_asset
check (projectUuid is not null or parentItemUuid is not null)
);
--//

View File

@ -29,35 +29,34 @@ subgraph bookingItem["`**bookingItem**`"]
end
end
subgraph debitorRel["`**debitorRel**`"]
subgraph project["`**project**`"]
direction TB
style debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
style project fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph debitorRel:roles[ ]
style debitorRel:roles fill:#99bcdb,stroke:white
subgraph project:roles[ ]
style project:roles fill:#99bcdb,stroke:white
role:debitorRel:OWNER[[debitorRel:OWNER]]
role:debitorRel:ADMIN[[debitorRel:ADMIN]]
role:debitorRel:AGENT[[debitorRel:AGENT]]
role:debitorRel:TENANT[[debitorRel:TENANT]]
role:project:OWNER[[project:OWNER]]
role:project:ADMIN[[project:ADMIN]]
role:project:AGENT[[project:AGENT]]
role:project:TENANT[[project:TENANT]]
end
end
%% granting roles to roles
role:global:ADMIN -.-> role:debitorRel:OWNER
role:debitorRel:OWNER -.-> role:debitorRel:ADMIN
role:debitorRel:ADMIN -.-> role:debitorRel:AGENT
role:debitorRel:AGENT -.-> role:debitorRel:TENANT
role:debitorRel:AGENT ==> role:bookingItem:OWNER
role:project:OWNER -.-> role:project:ADMIN
role:project:ADMIN -.-> role:project:AGENT
role:project:AGENT -.-> role:project:TENANT
role:project:AGENT ==> role:bookingItem:OWNER
role:bookingItem:OWNER ==> role:bookingItem:ADMIN
role:debitorRel:AGENT ==> role:bookingItem:ADMIN
role:bookingItem:ADMIN ==> role:bookingItem:AGENT
role:bookingItem:AGENT ==> role:bookingItem:TENANT
role:bookingItem:TENANT ==> role:debitorRel:TENANT
role:bookingItem:TENANT ==> role:project:TENANT
%% granting permissions to roles
role:debitorRel:ADMIN ==> perm:bookingItem:INSERT
role:global:ADMIN ==> perm:bookingItem:INSERT
role:global:ADMIN ==> perm:bookingItem:DELETE
role:project:ADMIN ==> perm:bookingItem:INSERT
role:bookingItem:ADMIN ==> perm:bookingItem:UPDATE
role:bookingItem:TENANT ==> perm:bookingItem:SELECT

View File

@ -0,0 +1,277 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
-- ============================================================================
--changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('hs_booking_item');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
*/
create or replace procedure buildRbacSystemForHsBookingItem(
NEW hs_booking_item
)
language plpgsql as $$
declare
newProject hs_booking_project;
newParentItem hs_booking_item;
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_booking_project WHERE uuid = NEW.projectUuid INTO newProject;
SELECT * FROM hs_booking_item WHERE uuid = NEW.parentItemUuid INTO newParentItem;
perform createRoleWithGrants(
hsBookingItemOWNER(NEW),
incomingSuperRoles => array[
hsBookingItemAGENT(newParentItem),
hsBookingProjectAGENT(newProject)]
);
perform createRoleWithGrants(
hsBookingItemADMIN(NEW),
permissions => array['UPDATE'],
incomingSuperRoles => array[hsBookingItemOWNER(NEW)]
);
perform createRoleWithGrants(
hsBookingItemAGENT(NEW),
incomingSuperRoles => array[hsBookingItemADMIN(NEW)]
);
perform createRoleWithGrants(
hsBookingItemTENANT(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[hsBookingItemAGENT(NEW)],
outgoingSubRoles => array[
hsBookingItemTENANT(newParentItem),
hsBookingProjectTENANT(newProject)]
);
call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), globalAdmin());
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$;
/*
AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row.
*/
create or replace function insertTriggerForHsBookingItem_tf()
returns trigger
language plpgsql
strict as $$
begin
call buildRbacSystemForHsBookingItem(NEW);
return NEW;
end; $$;
create trigger insertTriggerForHsBookingItem_tg
after insert on hs_booking_item
for each row
execute procedure insertTriggerForHsBookingItem_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to global ----------------------------
/*
Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing global rows.
*/
do language plpgsql $$
declare
row global;
begin
call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising global rows');
FOR row IN SELECT * FROM global
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
globalADMIN());
END LOOP;
end;
$$;
/**
Grants hs_booking_item INSERT permission to specified role of new global rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_global_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
globalADMIN());
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_global_tg
after insert on global
for each row
execute procedure new_hs_booking_item_grants_insert_to_global_tf();
-- granting INSERT permission to hs_booking_project ----------------------------
/*
Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_booking_project rows.
*/
do language plpgsql $$
declare
row hs_booking_project;
begin
call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_booking_project rows');
FOR row IN SELECT * FROM hs_booking_project
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
hsBookingProjectADMIN(row));
END LOOP;
end;
$$;
/**
Grants hs_booking_item INSERT permission to specified role of new hs_booking_project rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_hs_booking_project_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
hsBookingProjectADMIN(NEW));
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_project_tg
after insert on hs_booking_project
for each row
execute procedure new_hs_booking_item_grants_insert_to_hs_booking_project_tf();
-- granting INSERT permission to hs_booking_item ----------------------------
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet.
/**
Grants hs_booking_item INSERT permission to specified role of new hs_booking_item rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_hs_booking_item_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
hsBookingItemADMIN(NEW));
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_item_tg
after insert on hs_booking_item
for each row
execute procedure new_hs_booking_item_grants_insert_to_hs_booking_item_tf();
-- ============================================================================
--changeset hs_booking_item-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_item.
*/
create or replace function hs_booking_item_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
-- check INSERT INSERT if global ADMIN
if isGlobalAdmin() then
return NEW;
end if;
-- check INSERT permission via direct foreign key: NEW.projectUuid
if hasInsertPermission(NEW.projectUuid, 'hs_booking_item') then
return NEW;
end if;
-- check INSERT permission via direct foreign key: NEW.parentItemUuid
if hasInsertPermission(NEW.parentItemUuid, 'hs_booking_item') then
return NEW;
end if;
raise exception '[403] insert into hs_booking_item values(%) not allowed for current subjects % (%)',
NEW, currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger hs_booking_item_insert_permission_check_tg
before insert on hs_booking_item
for each row
execute procedure hs_booking_item_insert_permission_check_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacIdentityViewFromProjection('hs_booking_item',
$idName$
caption
$idName$);
--//
-- ============================================================================
--changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_booking_item',
$orderBy$
validity
$orderBy$,
$updates$
version = new.version,
caption = new.caption,
validity = new.validity,
resources = new.resources
$updates$);
--//

View File

@ -0,0 +1,58 @@
--liquibase formatted sql
-- ============================================================================
--changeset hs-booking-item-TEST-DATA-GENERATOR:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a single hs_booking_item test record.
*/
create or replace procedure createHsBookingItemTransactionTestData(
givenPartnerNumber numeric,
givenDebitorSuffix char(2)
)
language plpgsql as $$
declare
currentTask varchar;
relatedProject hs_booking_project;
privateCloudUuid uuid;
managedServerUuid uuid;
begin
currentTask := 'creating booking-item test-data ' || givenPartnerNumber::text || givenDebitorSuffix;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
execute format('set local hsadminng.currentTask to %L', currentTask);
select project.* into relatedProject
from hs_booking_project project
where project.caption = 'D-' || givenPartnerNumber || givenDebitorSuffix || ' default project';
raise notice 'creating test booking-item: %', givenPartnerNumber::text || givenDebitorSuffix::text;
raise notice '- using project (%): %', relatedProject.uuid, relatedProject;
privateCloudUuid := uuid_generate_v4();
managedServerUuid := uuid_generate_v4();
insert
into hs_booking_item (uuid, projectuuid, type, parentitemuuid, caption, validity, resources)
values (privateCloudUuid, relatedProject.uuid, 'PRIVATE_CLOUD', null, 'some PrivateCloud', daterange('20240401', null, '[]'), '{ "CPUs": 10, "RAM": 32, "SSD": 4000, "HDD": 10000, "Traffic": 2000 }'::jsonb),
(uuid_generate_v4(), null, 'MANAGED_SERVER', privateCloudUuid, 'some ManagedServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 500, "Traffic": 500 }'::jsonb),
(uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'test CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 2, "RAM": 4, "SSD": 750, "Traffic": 500 }'::jsonb),
(uuid_generate_v4(), null, 'CLOUD_SERVER', privateCloudUuid, 'prod CloudServer', daterange('20230115', '20240415', '[)'), '{ "CPUs": 4, "RAM": 16, "SSD": 1000, "Traffic": 500 }'::jsonb),
(managedServerUuid, relatedProject.uuid, 'MANAGED_SERVER', null, 'separate ManagedServer', daterange('20221001', null, '[]'), '{ "CPUs": 2, "RAM": 8, "SSD": 500, "Traffic": 500 }'::jsonb),
(uuid_generate_v4(), null, 'MANAGED_WEBSPACE', managedServerUuid, 'some ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 50, "Traffic": 20, "Daemons": 2, "Multi": 4 }'::jsonb),
(uuid_generate_v4(), relatedProject.uuid, 'MANAGED_WEBSPACE', null, 'separate ManagedWebspace', daterange('20221001', null, '[]'), '{ "SSD": 100, "Traffic": 50, "Daemons": 0, "Multi": 1 }'::jsonb);
end; $$;
--//
-- ============================================================================
--changeset hs-booking-item-TEST-DATA-GENERATION:1 context=dev,tc endDelimiter:--//
-- ----------------------------------------------------------------------------
do language plpgsql $$
begin
call createHsBookingItemTransactionTestData(10001, '11');
call createHsBookingItemTransactionTestData(10002, '12');
call createHsBookingItemTransactionTestData(10003, '13');
end;
$$;
--//

View File

@ -0,0 +1,63 @@
### rbac bookingItem
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph bookingItem["`**bookingItem**`"]
direction TB
style bookingItem fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph bookingItem:roles[ ]
style bookingItem:roles fill:#dd4901,stroke:white
role:bookingItem:OWNER[[bookingItem:OWNER]]
role:bookingItem:ADMIN[[bookingItem:ADMIN]]
role:bookingItem:AGENT[[bookingItem:AGENT]]
role:bookingItem:TENANT[[bookingItem:TENANT]]
end
subgraph bookingItem:permissions[ ]
style bookingItem:permissions fill:#dd4901,stroke:white
perm:bookingItem:INSERT{{bookingItem:INSERT}}
perm:bookingItem:DELETE{{bookingItem:DELETE}}
perm:bookingItem:UPDATE{{bookingItem:UPDATE}}
perm:bookingItem:SELECT{{bookingItem:SELECT}}
end
end
subgraph project["`**project**`"]
direction TB
style project fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph project:roles[ ]
style project:roles fill:#99bcdb,stroke:white
role:project:OWNER[[project:OWNER]]
role:project:ADMIN[[project:ADMIN]]
role:project:AGENT[[project:AGENT]]
role:project:TENANT[[project:TENANT]]
end
end
%% granting roles to roles
role:project:OWNER -.-> role:project:ADMIN
role:project:ADMIN -.-> role:project:AGENT
role:project:AGENT -.-> role:project:TENANT
role:project:AGENT ==> role:bookingItem:OWNER
role:bookingItem:OWNER ==> role:bookingItem:ADMIN
role:bookingItem:ADMIN ==> role:bookingItem:AGENT
role:bookingItem:AGENT ==> role:bookingItem:TENANT
role:bookingItem:TENANT ==> role:project:TENANT
%% granting permissions to roles
role:global:ADMIN ==> perm:bookingItem:INSERT
role:global:ADMIN ==> perm:bookingItem:DELETE
role:project:ADMIN ==> perm:bookingItem:INSERT
role:bookingItem:ADMIN ==> perm:bookingItem:UPDATE
role:bookingItem:TENANT ==> perm:bookingItem:SELECT
```

View File

@ -0,0 +1,277 @@
--liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
-- ============================================================================
--changeset hs-booking-item-rbac-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRelatedRbacObject('hs_booking_item');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRoleDescriptors('hsBookingItem', 'hs_booking_item');
--//
-- ============================================================================
--changeset hs-booking-item-rbac-insert-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
*/
create or replace procedure buildRbacSystemForHsBookingItem(
NEW hs_booking_item
)
language plpgsql as $$
declare
newProject hs_booking_project;
newParentItem hs_booking_item;
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_booking_project WHERE uuid = NEW.projectUuid INTO newProject;
SELECT * FROM hs_booking_item WHERE uuid = NEW.parentItemUuid INTO newParentItem;
perform createRoleWithGrants(
hsBookingItemOWNER(NEW),
incomingSuperRoles => array[
hsBookingItemAGENT(newParentItem),
hsBookingProjectAGENT(newProject)]
);
perform createRoleWithGrants(
hsBookingItemADMIN(NEW),
permissions => array['UPDATE'],
incomingSuperRoles => array[hsBookingItemOWNER(NEW)]
);
perform createRoleWithGrants(
hsBookingItemAGENT(NEW),
incomingSuperRoles => array[hsBookingItemADMIN(NEW)]
);
perform createRoleWithGrants(
hsBookingItemTENANT(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[hsBookingItemAGENT(NEW)],
outgoingSubRoles => array[
hsBookingItemTENANT(newParentItem),
hsBookingProjectTENANT(newProject)]
);
call grantPermissionToRole(createPermission(NEW.uuid, 'DELETE'), globalAdmin());
call leaveTriggerForObjectUuid(NEW.uuid);
end; $$;
/*
AFTER INSERT TRIGGER to create the role+grant structure for a new hs_booking_item row.
*/
create or replace function insertTriggerForHsBookingItem_tf()
returns trigger
language plpgsql
strict as $$
begin
call buildRbacSystemForHsBookingItem(NEW);
return NEW;
end; $$;
create trigger insertTriggerForHsBookingItem_tg
after insert on hs_booking_item
for each row
execute procedure insertTriggerForHsBookingItem_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to global ----------------------------
/*
Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing global rows.
*/
do language plpgsql $$
declare
row global;
begin
call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising global rows');
FOR row IN SELECT * FROM global
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
globalADMIN());
END LOOP;
end;
$$;
/**
Grants hs_booking_item INSERT permission to specified role of new global rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_global_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
globalADMIN());
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_global_tg
after insert on global
for each row
execute procedure new_hs_booking_item_grants_insert_to_global_tf();
-- granting INSERT permission to hs_booking_project ----------------------------
/*
Grants INSERT INTO hs_booking_item permissions to specified role of pre-existing hs_booking_project rows.
*/
do language plpgsql $$
declare
row hs_booking_project;
begin
call defineContext('create INSERT INTO hs_booking_item permissions for pre-exising hs_booking_project rows');
FOR row IN SELECT * FROM hs_booking_project
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_booking_item'),
hsBookingProjectADMIN(row));
END LOOP;
end;
$$;
/**
Grants hs_booking_item INSERT permission to specified role of new hs_booking_project rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_hs_booking_project_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
hsBookingProjectADMIN(NEW));
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_project_tg
after insert on hs_booking_project
for each row
execute procedure new_hs_booking_item_grants_insert_to_hs_booking_project_tf();
-- granting INSERT permission to hs_booking_item ----------------------------
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet.
/**
Grants hs_booking_item INSERT permission to specified role of new hs_booking_item rows.
*/
create or replace function new_hs_booking_item_grants_insert_to_hs_booking_item_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_booking_item'),
hsBookingItemADMIN(NEW));
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_booking_item_grants_insert_to_hs_booking_item_tg
after insert on hs_booking_item
for each row
execute procedure new_hs_booking_item_grants_insert_to_hs_booking_item_tf();
-- ============================================================================
--changeset hs_booking_item-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to hs_booking_item.
*/
create or replace function hs_booking_item_insert_permission_check_tf()
returns trigger
language plpgsql as $$
declare
superObjectUuid uuid;
begin
-- check INSERT INSERT if global ADMIN
if isGlobalAdmin() then
return NEW;
end if;
-- check INSERT permission via direct foreign key: NEW.projectUuid
if hasInsertPermission(NEW.projectUuid, 'hs_booking_item') then
return NEW;
end if;
-- check INSERT permission via direct foreign key: NEW.parentItemUuid
if hasInsertPermission(NEW.parentItemUuid, 'hs_booking_item') then
return NEW;
end if;
raise exception '[403] insert into hs_booking_item values(%) not allowed for current subjects % (%)',
NEW, currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger hs_booking_item_insert_permission_check_tg
before insert on hs_booking_item
for each row
execute procedure hs_booking_item_insert_permission_check_tf();
--//
-- ============================================================================
--changeset hs-booking-item-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacIdentityViewFromProjection('hs_booking_item',
$idName$
caption
$idName$);
--//
-- ============================================================================
--changeset hs-booking-item-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacRestrictedView('hs_booking_item',
$orderBy$
validity
$orderBy$,
$updates$
version = new.version,
caption = new.caption,
validity = new.validity,
resources = new.resources
$updates$);
--//

View File

@ -10,6 +10,10 @@ create type HsHostingAssetType as enum (
'MANAGED_WEBSPACE',
'UNIX_USER',
'DOMAIN_SETUP',
'DOMAIN_DNS_SETUP',
'DOMAIN_HTTP_SETUP',
'DOMAIN_SMTP_SETUP',
'DOMAIN_MBOX_SETUP',
'EMAIL_ALIAS',
'EMAIL_ADDRESS',
'PGSQL_USER',
@ -26,12 +30,15 @@ create table if not exists hs_hosting_asset
version int not null default 0,
bookingItemUuid uuid null references hs_booking_item(uuid),
type HsHostingAssetType not null,
parentAssetUuid uuid null references hs_hosting_asset(uuid),
parentAssetUuid uuid null references hs_hosting_asset(uuid) initially deferred,
assignedToAssetUuid uuid null references hs_hosting_asset(uuid) initially deferred,
identifier varchar(80) not null,
caption varchar(80) not null,
caption varchar(80),
config jsonb not null,
alarmContactUuid uuid null references hs_office_contact(uuid) initially deferred,
constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset check (bookingItemUuid is not null or parentAssetUuid is not null)
constraint chk_hs_hosting_asset_has_booking_item_or_parent_asset
check (bookingItemUuid is not null or parentAssetUuid is not null or type='DOMAIN_SETUP')
);
--//
@ -58,9 +65,13 @@ begin
when 'MANAGED_SERVER' then null
when 'MANAGED_WEBSPACE' then 'MANAGED_SERVER'
when 'UNIX_USER' then 'MANAGED_WEBSPACE'
when 'DOMAIN_SETUP' then 'UNIX_USER'
when 'EMAIL_ALIAS' then 'MANAGED_WEBSPACE'
when 'EMAIL_ADDRESS' then 'DOMAIN_SETUP'
when 'DOMAIN_SETUP' then null
when 'DOMAIN_DNS_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_HTTP_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_SMTP_SETUP' then 'DOMAIN_SETUP'
when 'DOMAIN_MBOX_SETUP' then 'DOMAIN_SETUP'
when 'EMAIL_ADDRESS' then 'DOMAIN_MBOX_SETUP'
when 'PGSQL_USER' then 'MANAGED_WEBSPACE'
when 'PGSQL_DATABASE' then 'MANAGED_WEBSPACE'
when 'MARIADB_USER' then 'MANAGED_WEBSPACE'
@ -69,10 +80,10 @@ begin
end);
if expectedParentType is not null and actualParentType is null then
raise exception '[400] % must have % as parent, but got <NULL>',
raise exception '[400] HostingAsset % must have % as parent, but got <NULL>',
NEW.type, expectedParentType;
elsif expectedParentType is not null and actualParentType <> expectedParentType then
raise exception '[400] % must have % as parent, but got %s',
raise exception '[400] HostingAsset % must have % as parent, but got %s',
NEW.type, expectedParentType, actualParentType;
end if;
return NEW;
@ -94,27 +105,23 @@ create or replace function hs_hosting_asset_booking_item_hierarchy_check_tf()
language plpgsql as $$
declare
actualBookingItemType HsBookingItemType;
expectedBookingItemTypes HsBookingItemType[];
expectedBookingItemType HsBookingItemType;
begin
actualBookingItemType := (select type
from hs_booking_item
where NEW.bookingItemUuid = uuid);
if NEW.type = 'CLOUD_SERVER' then
expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'CLOUD_SERVER'];
expectedBookingItemType := 'CLOUD_SERVER';
elsif NEW.type = 'MANAGED_SERVER' then
expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER'];
expectedBookingItemType := 'MANAGED_SERVER';
elsif NEW.type = 'MANAGED_WEBSPACE' then
if NEW.parentAssetUuid is null then
expectedBookingItemTypes := ARRAY['MANAGED_WEBSPACE'];
else
expectedBookingItemTypes := ARRAY['PRIVATE_CLOUD', 'MANAGED_SERVER'];
end if;
expectedBookingItemType := 'MANAGED_WEBSPACE';
end if;
if not actualBookingItemType = any(expectedBookingItemTypes) then
raise exception '[400] % % must have any of % as booking-item, but got %',
NEW.type, NEW.identifier, expectedBookingItemTypes, actualBookingItemType;
if not actualBookingItemType = expectedBookingItemType then
raise exception '[400] HostingAsset % % must have % as booking-item, but got %',
NEW.type, NEW.identifier, expectedBookingItemType, actualBookingItemType;
end if;
return NEW;
end; $$;

View File

@ -1,92 +0,0 @@
### rbac asset inCaseOf:CLOUD_SERVER
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph asset["`**asset**`"]
direction TB
style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph asset:roles[ ]
style asset:roles fill:#dd4901,stroke:white
role:asset:OWNER[[asset:OWNER]]
role:asset:ADMIN[[asset:ADMIN]]
role:asset:TENANT[[asset:TENANT]]
end
subgraph asset:permissions[ ]
style asset:permissions fill:#dd4901,stroke:white
perm:asset:INSERT{{asset:INSERT}}
perm:asset:DELETE{{asset:DELETE}}
perm:asset:UPDATE{{asset:UPDATE}}
perm:asset:SELECT{{asset:SELECT}}
end
end
subgraph bookingItem["`**bookingItem**`"]
direction TB
style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem:roles[ ]
style bookingItem:roles fill:#99bcdb,stroke:white
role:bookingItem:OWNER[[bookingItem:OWNER]]
role:bookingItem:ADMIN[[bookingItem:ADMIN]]
role:bookingItem:AGENT[[bookingItem:AGENT]]
role:bookingItem:TENANT[[bookingItem:TENANT]]
end
end
subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"]
direction TB
style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem.debitorRel:roles[ ]
style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white
role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]]
role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]]
role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]]
role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]]
end
end
subgraph parentServer["`**parentServer**`"]
direction TB
style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph parentServer:roles[ ]
style parentServer:roles fill:#99bcdb,stroke:white
role:parentServer:ADMIN[[parentServer:ADMIN]]
end
end
%% granting roles to roles
role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER
role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN
role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER
role:bookingItem:OWNER -.-> role:bookingItem:ADMIN
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN
role:bookingItem:ADMIN -.-> role:bookingItem:AGENT
role:bookingItem:AGENT -.-> role:bookingItem:TENANT
role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem:ADMIN ==> role:asset:OWNER
role:asset:OWNER ==> role:asset:ADMIN
role:asset:ADMIN ==> role:asset:TENANT
role:asset:TENANT ==> role:bookingItem:TENANT
%% granting permissions to roles
role:bookingItem:AGENT ==> perm:asset:INSERT
role:asset:OWNER ==> perm:asset:DELETE
role:asset:ADMIN ==> perm:asset:UPDATE
role:asset:TENANT ==> perm:asset:SELECT
```

View File

@ -1,92 +0,0 @@
### rbac asset inCaseOf:MANAGED_SERVER
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph asset["`**asset**`"]
direction TB
style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph asset:roles[ ]
style asset:roles fill:#dd4901,stroke:white
role:asset:OWNER[[asset:OWNER]]
role:asset:ADMIN[[asset:ADMIN]]
role:asset:TENANT[[asset:TENANT]]
end
subgraph asset:permissions[ ]
style asset:permissions fill:#dd4901,stroke:white
perm:asset:INSERT{{asset:INSERT}}
perm:asset:DELETE{{asset:DELETE}}
perm:asset:UPDATE{{asset:UPDATE}}
perm:asset:SELECT{{asset:SELECT}}
end
end
subgraph bookingItem["`**bookingItem**`"]
direction TB
style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem:roles[ ]
style bookingItem:roles fill:#99bcdb,stroke:white
role:bookingItem:OWNER[[bookingItem:OWNER]]
role:bookingItem:ADMIN[[bookingItem:ADMIN]]
role:bookingItem:AGENT[[bookingItem:AGENT]]
role:bookingItem:TENANT[[bookingItem:TENANT]]
end
end
subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"]
direction TB
style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem.debitorRel:roles[ ]
style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white
role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]]
role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]]
role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]]
role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]]
end
end
subgraph parentServer["`**parentServer**`"]
direction TB
style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph parentServer:roles[ ]
style parentServer:roles fill:#99bcdb,stroke:white
role:parentServer:ADMIN[[parentServer:ADMIN]]
end
end
%% granting roles to roles
role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER
role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN
role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER
role:bookingItem:OWNER -.-> role:bookingItem:ADMIN
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN
role:bookingItem:ADMIN -.-> role:bookingItem:AGENT
role:bookingItem:AGENT -.-> role:bookingItem:TENANT
role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem:ADMIN ==> role:asset:OWNER
role:asset:OWNER ==> role:asset:ADMIN
role:asset:ADMIN ==> role:asset:TENANT
role:asset:TENANT ==> role:bookingItem:TENANT
%% granting permissions to roles
role:bookingItem:AGENT ==> perm:asset:INSERT
role:asset:OWNER ==> perm:asset:DELETE
role:asset:ADMIN ==> perm:asset:UPDATE
role:asset:TENANT ==> perm:asset:SELECT
```

View File

@ -1,93 +0,0 @@
### rbac asset inCaseOf:MANAGED_WEBSPACE
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
```mermaid
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph asset["`**asset**`"]
direction TB
style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px
subgraph asset:roles[ ]
style asset:roles fill:#dd4901,stroke:white
role:asset:OWNER[[asset:OWNER]]
role:asset:ADMIN[[asset:ADMIN]]
role:asset:TENANT[[asset:TENANT]]
end
subgraph asset:permissions[ ]
style asset:permissions fill:#dd4901,stroke:white
perm:asset:INSERT{{asset:INSERT}}
perm:asset:DELETE{{asset:DELETE}}
perm:asset:UPDATE{{asset:UPDATE}}
perm:asset:SELECT{{asset:SELECT}}
end
end
subgraph bookingItem["`**bookingItem**`"]
direction TB
style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem:roles[ ]
style bookingItem:roles fill:#99bcdb,stroke:white
role:bookingItem:OWNER[[bookingItem:OWNER]]
role:bookingItem:ADMIN[[bookingItem:ADMIN]]
role:bookingItem:AGENT[[bookingItem:AGENT]]
role:bookingItem:TENANT[[bookingItem:TENANT]]
end
end
subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"]
direction TB
style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem.debitorRel:roles[ ]
style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white
role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]]
role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]]
role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]]
role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]]
end
end
subgraph parentServer["`**parentServer**`"]
direction TB
style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph parentServer:roles[ ]
style parentServer:roles fill:#99bcdb,stroke:white
role:parentServer:ADMIN[[parentServer:ADMIN]]
end
end
%% granting roles to roles
role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER
role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN
role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER
role:bookingItem:OWNER -.-> role:bookingItem:ADMIN
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN
role:bookingItem:ADMIN -.-> role:bookingItem:AGENT
role:bookingItem:AGENT -.-> role:bookingItem:TENANT
role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem:ADMIN ==> role:asset:OWNER
role:asset:OWNER ==> role:asset:ADMIN
role:asset:ADMIN ==> role:asset:TENANT
role:asset:TENANT ==> role:bookingItem:TENANT
%% granting permissions to roles
role:bookingItem:AGENT ==> perm:asset:INSERT
role:parentServer:ADMIN ==> perm:asset:INSERT
role:asset:OWNER ==> perm:asset:DELETE
role:asset:ADMIN ==> perm:asset:UPDATE
role:asset:TENANT ==> perm:asset:SELECT
```

View File

@ -1,4 +1,4 @@
### rbac asset inOtherCases
### rbac asset
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
@ -6,6 +6,19 @@ This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manua
%%{init:{'flowchart':{'htmlLabels':false}}}%%
flowchart TB
subgraph alarmContact["`**alarmContact**`"]
direction TB
style alarmContact fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph alarmContact:roles[ ]
style alarmContact:roles fill:#99bcdb,stroke:white
role:alarmContact:OWNER[[alarmContact:OWNER]]
role:alarmContact:ADMIN[[alarmContact:ADMIN]]
role:alarmContact:REFERRER[[alarmContact:REFERRER]]
end
end
subgraph asset["`**asset**`"]
direction TB
style asset fill:#dd4901,stroke:#274d6e,stroke-width:8px
@ -15,6 +28,7 @@ subgraph asset["`**asset**`"]
role:asset:OWNER[[asset:OWNER]]
role:asset:ADMIN[[asset:ADMIN]]
role:asset:AGENT[[asset:AGENT]]
role:asset:TENANT[[asset:TENANT]]
end
@ -28,6 +42,17 @@ subgraph asset["`**asset**`"]
end
end
subgraph assignedToAsset["`**assignedToAsset**`"]
direction TB
style assignedToAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph assignedToAsset:roles[ ]
style assignedToAsset:roles fill:#99bcdb,stroke:white
role:assignedToAsset:TENANT[[assignedToAsset:TENANT]]
end
end
subgraph bookingItem["`**bookingItem**`"]
direction TB
style bookingItem fill:#99bcdb,stroke:#274d6e,stroke-width:8px
@ -42,48 +67,47 @@ subgraph bookingItem["`**bookingItem**`"]
end
end
subgraph bookingItem.debitorRel["`**bookingItem.debitorRel**`"]
subgraph parentAsset["`**parentAsset**`"]
direction TB
style bookingItem.debitorRel fill:#99bcdb,stroke:#274d6e,stroke-width:8px
style parentAsset fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph bookingItem.debitorRel:roles[ ]
style bookingItem.debitorRel:roles fill:#99bcdb,stroke:white
subgraph parentAsset:roles[ ]
style parentAsset:roles fill:#99bcdb,stroke:white
role:bookingItem.debitorRel:OWNER[[bookingItem.debitorRel:OWNER]]
role:bookingItem.debitorRel:ADMIN[[bookingItem.debitorRel:ADMIN]]
role:bookingItem.debitorRel:AGENT[[bookingItem.debitorRel:AGENT]]
role:bookingItem.debitorRel:TENANT[[bookingItem.debitorRel:TENANT]]
role:parentAsset:ADMIN[[parentAsset:ADMIN]]
role:parentAsset:AGENT[[parentAsset:AGENT]]
role:parentAsset:TENANT[[parentAsset:TENANT]]
end
end
subgraph parentServer["`**parentServer**`"]
direction TB
style parentServer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
subgraph parentServer:roles[ ]
style parentServer:roles fill:#99bcdb,stroke:white
role:parentServer:ADMIN[[parentServer:ADMIN]]
end
end
%% granting roles to users
user:creator ==> role:asset:OWNER
%% granting roles to roles
role:global:ADMIN -.-> role:bookingItem.debitorRel:OWNER
role:bookingItem.debitorRel:OWNER -.-> role:bookingItem.debitorRel:ADMIN
role:bookingItem.debitorRel:ADMIN -.-> role:bookingItem.debitorRel:AGENT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem.debitorRel:TENANT
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:OWNER
role:bookingItem:OWNER -.-> role:bookingItem:ADMIN
role:bookingItem.debitorRel:AGENT -.-> role:bookingItem:ADMIN
role:bookingItem:ADMIN -.-> role:bookingItem:AGENT
role:bookingItem:AGENT -.-> role:bookingItem:TENANT
role:bookingItem:TENANT -.-> role:bookingItem.debitorRel:TENANT
role:global:ADMIN -.-> role:alarmContact:OWNER
role:alarmContact:OWNER -.-> role:alarmContact:ADMIN
role:alarmContact:ADMIN -.-> role:alarmContact:REFERRER
role:global:ADMIN ==>|XX| role:asset:OWNER
role:bookingItem:ADMIN ==> role:asset:OWNER
role:parentAsset:ADMIN ==> role:asset:OWNER
role:asset:OWNER ==> role:asset:ADMIN
role:asset:ADMIN ==> role:asset:TENANT
role:bookingItem:AGENT ==> role:asset:ADMIN
role:parentAsset:AGENT ==> role:asset:ADMIN
role:asset:ADMIN ==> role:asset:AGENT
role:asset:AGENT ==> role:assignedToAsset:TENANT
role:asset:AGENT ==> role:alarmContact:REFERRER
role:asset:AGENT ==> role:asset:TENANT
role:asset:TENANT ==> role:bookingItem:TENANT
role:asset:TENANT ==> role:parentAsset:TENANT
role:alarmContact:ADMIN ==> role:asset:TENANT
%% granting permissions to roles
role:global:ADMIN ==> perm:asset:INSERT
role:parentAsset:ADMIN ==> perm:asset:INSERT
role:global:GUEST ==> perm:asset:INSERT
role:asset:OWNER ==> perm:asset:DELETE
role:asset:ADMIN ==> perm:asset:UPDATE
role:asset:TENANT ==> perm:asset:SELECT

View File

@ -30,39 +30,61 @@ create or replace procedure buildRbacSystemForHsHostingAsset(
language plpgsql as $$
declare
newParentServer hs_hosting_asset;
newBookingItem hs_booking_item;
newAssignedToAsset hs_hosting_asset;
newAlarmContact hs_office_contact;
newParentAsset hs_hosting_asset;
begin
call enterTriggerForObjectUuid(NEW.uuid);
SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentServer;
SELECT * FROM hs_booking_item WHERE uuid = NEW.bookingItemUuid INTO newBookingItem;
SELECT * FROM hs_hosting_asset WHERE uuid = NEW.assignedToAssetUuid INTO newAssignedToAsset;
SELECT * FROM hs_office_contact WHERE uuid = NEW.alarmContactUuid INTO newAlarmContact;
SELECT * FROM hs_hosting_asset WHERE uuid = NEW.parentAssetUuid INTO newParentAsset;
perform createRoleWithGrants(
hsHostingAssetOWNER(NEW),
permissions => array['DELETE'],
incomingSuperRoles => array[hsBookingItemADMIN(newBookingItem)]
incomingSuperRoles => array[
globalADMIN(unassumed()),
hsBookingItemADMIN(newBookingItem),
hsHostingAssetADMIN(newParentAsset)],
userUuids => array[currentUserUuid()]
);
perform createRoleWithGrants(
hsHostingAssetADMIN(NEW),
permissions => array['UPDATE'],
incomingSuperRoles => array[hsHostingAssetOWNER(NEW)]
incomingSuperRoles => array[
hsBookingItemAGENT(newBookingItem),
hsHostingAssetAGENT(newParentAsset),
hsHostingAssetOWNER(NEW)]
);
perform createRoleWithGrants(
hsHostingAssetAGENT(NEW),
incomingSuperRoles => array[hsHostingAssetADMIN(NEW)],
outgoingSubRoles => array[
hsHostingAssetTENANT(newAssignedToAsset),
hsOfficeContactREFERRER(newAlarmContact)]
);
perform createRoleWithGrants(
hsHostingAssetTENANT(NEW),
permissions => array['SELECT'],
incomingSuperRoles => array[hsHostingAssetADMIN(NEW)],
outgoingSubRoles => array[hsBookingItemTENANT(newBookingItem)]
incomingSuperRoles => array[
hsHostingAssetAGENT(NEW),
hsOfficeContactADMIN(newAlarmContact)],
outgoingSubRoles => array[
hsBookingItemTENANT(newBookingItem),
hsHostingAssetTENANT(newParentAsset)]
);
IF NEW.type = 'CLOUD_SERVER' THEN
ELSIF NEW.type = 'MANAGED_SERVER' THEN
ELSIF NEW.type = 'MANAGED_WEBSPACE' THEN
ELSE
IF NEW.type = 'DOMAIN_SETUP' THEN
END IF;
call leaveTriggerForObjectUuid(NEW.uuid);
@ -89,110 +111,44 @@ execute procedure insertTriggerForHsHostingAsset_tf();
-- ============================================================================
--changeset hs-hosting-asset-rbac-GRANTING-INSERT-PERMISSION:1 endDelimiter:--//
--changeset hs-hosting-asset-rbac-update-trigger:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
-- granting INSERT permission to hs_booking_item ----------------------------
/*
Grants INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_booking_item rows.
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
*/
do language plpgsql $$
declare
row hs_booking_item;
begin
call defineContext('create INSERT INTO hs_hosting_asset permissions for pre-exising hs_booking_item rows');
FOR row IN SELECT * FROM hs_booking_item
-- unconditional for all rows in that table
LOOP
call grantPermissionToRole(
createPermission(row.uuid, 'INSERT', 'hs_hosting_asset'),
hsBookingItemAGENT(row));
END LOOP;
end;
$$;
/**
Grants hs_hosting_asset INSERT permission to specified role of new hs_booking_item rows.
*/
create or replace function new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf()
returns trigger
language plpgsql
strict as $$
begin
-- unconditional for all rows in that table
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'),
hsBookingItemAGENT(NEW));
-- end.
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_hosting_asset_grants_insert_to_hs_booking_item_tg
after insert on hs_booking_item
for each row
execute procedure new_hs_hosting_asset_grants_insert_to_hs_booking_item_tf();
-- granting INSERT permission to hs_hosting_asset ----------------------------
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped,
-- because there cannot yet be any pre-existing rows in the same table yet.
/**
Grants hs_hosting_asset INSERT permission to specified role of new hs_hosting_asset rows.
*/
create or replace function new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf()
returns trigger
language plpgsql
strict as $$
begin
if NEW.type = 'MANAGED_SERVER' then
call grantPermissionToRole(
createPermission(NEW.uuid, 'INSERT', 'hs_hosting_asset'),
hsHostingAssetADMIN(NEW));
end if;
return NEW;
end; $$;
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
create trigger z_new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tg
after insert on hs_hosting_asset
for each row
execute procedure new_hs_hosting_asset_grants_insert_to_hs_hosting_asset_tf();
-- ============================================================================
--changeset hs_hosting_asset-rbac-CHECKING-INSERT-PERMISSION:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Checks if the user respectively the assumed roles are allowed to insert a row to hs_hosting_asset.
*/
create or replace function hs_hosting_asset_insert_permission_check_tf()
returns trigger
create or replace procedure updateRbacRulesForHsHostingAsset(
OLD hs_hosting_asset,
NEW hs_hosting_asset
)
language plpgsql as $$
declare
superObjectUuid uuid;
begin
-- check INSERT permission via direct foreign key: NEW.bookingItemUuid
if NEW.type in ('MANAGED_SERVER', 'CLOUD_SERVER', 'MANAGED_WEBSPACE') and hasInsertPermission(NEW.bookingItemUuid, 'hs_hosting_asset') then
return NEW;
end if;
-- check INSERT permission via direct foreign key: NEW.parentAssetUuid
if NEW.type in ('MANAGED_WEBSPACE') and hasInsertPermission(NEW.parentAssetUuid, 'hs_hosting_asset') then
return NEW;
end if;
raise exception '[403] insert into hs_hosting_asset not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
if NEW.assignedToAssetUuid is distinct from OLD.assignedToAssetUuid
or NEW.alarmContactUuid is distinct from OLD.alarmContactUuid then
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
call buildRbacSystemForHsHostingAsset(NEW);
end if;
end; $$;
create trigger hs_hosting_asset_insert_permission_check_tg
before insert on hs_hosting_asset
/*
AFTER INSERT TRIGGER to re-wire the grant structure for a new hs_hosting_asset row.
*/
create or replace function updateTriggerForHsHostingAsset_tf()
returns trigger
language plpgsql
strict as $$
begin
call updateRbacRulesForHsHostingAsset(OLD, NEW);
return NEW;
end; $$;
create trigger updateTriggerForHsHostingAsset_tg
after update on hs_hosting_asset
for each row
execute procedure hs_hosting_asset_insert_permission_check_tf();
execute procedure updateTriggerForHsHostingAsset_tf();
--//
@ -200,11 +156,9 @@ create trigger hs_hosting_asset_insert_permission_check_tg
--changeset hs-hosting-asset-rbac-IDENTITY-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
call generateRbacIdentityViewFromQuery('hs_hosting_asset',
call generateRbacIdentityViewFromProjection('hs_hosting_asset',
$idName$
SELECT asset.uuid as uuid, bookingItemIV.idName || '-' || cleanIdentifier(asset.identifier) as idName
FROM hs_hosting_asset asset
JOIN hs_booking_item_iv bookingItemIV ON bookingItemIV.uuid = asset.bookingItemUuid
identifier
$idName$);
--//
@ -219,7 +173,9 @@ call generateRbacRestrictedView('hs_hosting_asset',
$updates$
version = new.version,
caption = new.caption,
config = new.config
config = new.config,
assignedToAssetUuid = new.assignedToAssetUuid,
alarmContactUuid = new.alarmContactUuid
$updates$);
--//

View File

@ -8,46 +8,83 @@
/*
Creates a single hs_hosting_asset test record.
*/
create or replace procedure createHsHostingAssetTestData(
givenPartnerNumber numeric,
givenDebitorSuffix char(2),
givenWebspacePrefix char(3)
)
create or replace procedure createHsHostingAssetTestData(givenProjectCaption varchar)
language plpgsql as $$
declare
currentTask varchar;
relatedProject hs_booking_project;
relatedDebitor hs_office_debitor;
relatedPrivateCloudBookingItem hs_booking_item;
relatedManagedServerBookingItem hs_booking_item;
privateCloudBI hs_booking_item;
managedServerBI hs_booking_item;
cloudServerBI hs_booking_item;
managedWebspaceBI hs_booking_item;
debitorNumberSuffix varchar;
defaultPrefix varchar;
managedServerUuid uuid;
managedWebspaceUuid uuid;
webUnixUserUuid uuid;
domainSetupUuid uuid;
domainMBoxSetupUuid uuid;
begin
currentTask := 'creating hosting-asset test-data ' || givenPartnerNumber::text || givenDebitorSuffix;
currentTask := 'creating hosting-asset test-data ' || givenProjectCaption;
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
execute format('set local hsadminng.currentTask to %L', currentTask);
select project.* into relatedProject
from hs_booking_project project
where project.caption = givenProjectCaption;
assert relatedProject.uuid is not null, 'relatedProject for "' || givenProjectCaption || '" must not be null';
select debitor.* into relatedDebitor
from hs_office_debitor debitor
join hs_office_relation debitorRel on debitorRel.uuid = debitor.debitorRelUuid
join hs_office_relation partnerRel on partnerRel.holderUuid = debitorRel.anchorUuid
join hs_office_partner partner on partner.partnerRelUuid = partnerRel.uuid
where partner.partnerNumber = givenPartnerNumber and debitor.debitorNumberSuffix = givenDebitorSuffix;
select item.uuid into relatedPrivateCloudBookingItem
from hs_booking_item item
where item.debitoruuid = relatedDebitor.uuid
and item.type = 'PRIVATE_CLOUD';
select item.uuid into relatedManagedServerBookingItem
from hs_booking_item item
where item.debitoruuid = relatedDebitor.uuid
and item.type = 'MANAGED_SERVER';
select uuid_generate_v4() into managedServerUuid;
where debitor.uuid = relatedProject.debitorUuid;
assert relatedDebitor.uuid is not null, 'relatedDebitor for "' || givenProjectCaption || '" must not be null';
select item.* into privateCloudBI
from hs_booking_item item
where item.projectUuid = relatedProject.uuid
and item.type = 'PRIVATE_CLOUD';
assert privateCloudBI.uuid is not null, 'relatedPrivateCloudBookingItem for "' || givenProjectCaption|| '" must not be null';
select item.* into managedServerBI
from hs_booking_item item
where item.projectUuid = relatedProject.uuid
and item.type = 'MANAGED_SERVER';
assert managedServerBI.uuid is not null, 'relatedManagedServerBookingItem for "' || givenProjectCaption|| '" must not be null';
select item.* into cloudServerBI
from hs_booking_item item
where item.parentItemuuid = privateCloudBI.uuid
and item.type = 'CLOUD_SERVER';
assert cloudServerBI.uuid is not null, 'relatedCloudServerBookingItem for "' || givenProjectCaption|| '" must not be null';
select item.* into managedWebspaceBI
from hs_booking_item item
where item.projectUuid = relatedProject.uuid
and item.type = 'MANAGED_WEBSPACE';
assert managedWebspaceBI.uuid is not null, 'relatedManagedWebspaceBookingItem for "' || givenProjectCaption|| '" must not be null';
select uuid_generate_v4() into managedServerUuid;
select uuid_generate_v4() into managedWebspaceUuid;
select uuid_generate_v4() into webUnixUserUuid;
select uuid_generate_v4() into domainSetupUuid;
select uuid_generate_v4() into domainMBoxSetupUuid;
debitorNumberSuffix := relatedDebitor.debitorNumberSuffix;
defaultPrefix := relatedDebitor.defaultPrefix;
raise notice 'creating test hosting-asset: %', givenPartnerNumber::text || givenDebitorSuffix::text;
raise notice '- using debitor (%): %', relatedDebitor.uuid, relatedDebitor;
insert into hs_hosting_asset
(uuid, bookingitemuuid, type, parentAssetUuid, identifier, caption, config)
values (managedServerUuid, relatedPrivateCloudBookingItem.uuid, 'MANAGED_SERVER', null, 'vm10' || givenDebitorSuffix, 'some ManagedServer', '{ "CPU": 2, "SDD": 512, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedPrivateCloudBookingItem.uuid, 'CLOUD_SERVER', null, 'vm20' || givenDebitorSuffix, 'another CloudServer', '{ "CPU": 2, "HDD": 1024, "extra": 42 }'::jsonb),
(uuid_generate_v4(), relatedManagedServerBookingItem.uuid, 'MANAGED_WEBSPACE', managedServerUuid, givenWebspacePrefix || '01', 'some Webspace', '{ "RAM": 1, "SDD": 512, "HDD": 2048, "extra": 42 }'::jsonb);
(uuid, bookingitemuuid, type, parentAssetUuid, assignedToAssetUuid, identifier, caption, config)
values (managedServerUuid, managedServerBI.uuid, 'MANAGED_SERVER', null, null, 'vm10' || debitorNumberSuffix, 'some ManagedServer', '{ "monit_max_cpu_usage": 90, "monit_max_ram_usage": 80, "monit_max_ssd_usage": 70 }'::jsonb),
(uuid_generate_v4(), cloudServerBI.uuid, 'CLOUD_SERVER', null, null, 'vm20' || debitorNumberSuffix, 'another CloudServer', '{}'::jsonb),
(managedWebspaceUuid, managedWebspaceBI.uuid, 'MANAGED_WEBSPACE', managedServerUuid, null, defaultPrefix || '01', 'some Webspace', '{}'::jsonb),
(uuid_generate_v4(), null, 'EMAIL_ALIAS', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some E-Mail-Alias', '{ "target": [ "office@example.org", "archive@example.com" ] }'::jsonb),
(webUnixUserUuid, null, 'UNIX_USER', managedWebspaceUuid, null, defaultPrefix || '01-web', 'some UnixUser for Website', '{ "SSD-soft-quota": "128", "SSD-hard-quota": "256", "HDD-soft-quota": "512", "HDD-hard-quota": "1024"}'::jsonb),
(domainSetupUuid, null, 'DOMAIN_SETUP', null, null, defaultPrefix || '.example.org', 'some Domain-Setup', '{}'::jsonb),
(uuid_generate_v4(), null, 'DOMAIN_DNS_SETUP', domainSetupUuid, null, defaultPrefix || '.example.org|DNS', 'some Domain-DNS-Setup', '{}'::jsonb),
(uuid_generate_v4(), null, 'DOMAIN_HTTP_SETUP', domainSetupUuid, webUnixUserUuid, defaultPrefix || '.example.org|HTTP', 'some Domain-HTTP-Setup', '{ "option-htdocsfallback": true, "use-fcgiphpbin": "/usr/lib/cgi-bin/php", "validsubdomainnames": "*"}'::jsonb),
(uuid_generate_v4(), null, 'DOMAIN_SMTP_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-SMPT-Setup', '{}'::jsonb),
(domainMBoxSetupUuid, null, 'DOMAIN_MBOX_SETUP', domainSetupUuid, managedWebspaceUuid, defaultPrefix || '.example.org|DNS', 'some Domain-MBOX-Setup', '{}'::jsonb),
(uuid_generate_v4(), null, 'EMAIL_ADDRESS', domainMBoxSetupUuid, null, 'test@' || defaultPrefix || '.example.org', 'some E-Mail-Address', '{}'::jsonb);
end; $$;
--//
@ -58,9 +95,9 @@ end; $$;
do language plpgsql $$
begin
call createHsHostingAssetTestData(10001, '11', 'aaa');
call createHsHostingAssetTestData(10002, '12', 'bbb');
call createHsHostingAssetTestData(10003, '13', 'ccc');
call createHsHostingAssetTestData('D-1000111 default project');
call createHsHostingAssetTestData('D-1000212 default project');
call createHsHostingAssetTestData('D-1000313 default project');
end;
$$;
--//

View File

@ -130,11 +130,19 @@ databaseChangeLog:
- include:
file: db/changelog/5-hs-office/512-coopassets/5128-hs-office-coopassets-test-data.sql
- include:
file: db/changelog/6-hs-booking/601-booking-item/6010-hs-booking-item.sql
file: db/changelog/6-hs-booking/610-booking-debitor/6100-hs-booking-debitor.sql
- include:
file: db/changelog/6-hs-booking/601-booking-item/6013-hs-booking-item-rbac.sql
file: db/changelog/6-hs-booking/620-booking-project/6200-hs-booking-project.sql
- include:
file: db/changelog/6-hs-booking/601-booking-item/6018-hs-booking-item-test-data.sql
file: db/changelog/6-hs-booking/620-booking-project/6203-hs-booking-project-rbac.sql
- include:
file: db/changelog/6-hs-booking/620-booking-project/6208-hs-booking-project-test-data.sql
- include:
file: db/changelog/6-hs-booking/630-booking-item/6200-hs-booking-item.sql
- include:
file: db/changelog/6-hs-booking/630-booking-item/6203-hs-booking-item-rbac.sql
- include:
file: db/changelog/6-hs-booking/630-booking-item/6208-hs-booking-item-test-data.sql
- include:
file: db/changelog/7-hs-hosting/701-hosting-asset/7010-hs-hosting-asset.sql
- include:

View File

@ -8,7 +8,10 @@ import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemEntity;
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetEntity;
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import org.springframework.data.repository.Repository;
import org.springframework.web.bind.annotation.RestController;
@ -37,8 +40,11 @@ public class ArchitectureTest {
"..test.pac",
"..test.dom",
"..context",
"..hash",
"..generated..",
"..persistence..",
"..system..",
"..validation..",
"..hs.office.bankaccount",
"..hs.office.contact",
"..hs.office.coopassets",
@ -50,9 +56,12 @@ public class ArchitectureTest {
"..hs.office.person",
"..hs.office.relation",
"..hs.office.sepamandate",
"..hs.booking.debitor",
"..hs.booking.project",
"..hs.booking.item",
"..hs.booking.item.validators",
"..hs.hosting.asset",
"..hs.hosting.asset.validator",
"..hs.hosting.asset.validators",
"..errors",
"..mapper",
"..ping",
@ -103,6 +112,13 @@ public class ArchitectureTest {
.should().onlyDependOnClassesThat()
.resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG);
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule hashPackageRule = classes()
.that().resideInAPackage("..hash..")
.should().onlyDependOnClassesThat()
.resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG);
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule errorsPackageRule = classes()
@ -110,6 +126,13 @@ public class ArchitectureTest {
.should().onlyDependOnClassesThat()
.resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG);
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule systemPackageRule = classes()
.that().resideInAPackage("..system..")
.should().onlyDependOnClassesThat()
.resideOutsideOfPackage(NET_HOSTSHARING_HSADMINNG);
@ArchTest
@SuppressWarnings("unused")
public static final ArchRule testPackagesRule = classes()
@ -143,7 +166,8 @@ public class ArchitectureTest {
.should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage(
"..hs.booking.(*)..",
"..hs.hosting.(*).."
"..hs.hosting.(*)..",
"..hs.validation" // TODO.impl: Some Validators need to be refactored to booking package.
);
@ArchTest
@ -152,7 +176,8 @@ public class ArchitectureTest {
.that().resideInAPackage("..hs.hosting.(*)..")
.should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage(
"..hs.hosting.(*).."
"..hs.hosting.(*)..",
"..hs.booking.(*).." // TODO.impl: fix this cyclic dependency
);
@ArchTest
@ -187,7 +212,9 @@ public class ArchitectureTest {
"..hs.office.partner..",
"..hs.office.debitor..",
"..hs.office.membership..",
"..hs.office.migration..");
"..hs.office.migration..",
"..hs.hosting.asset.."
);
@ArchTest
@SuppressWarnings("unused")
@ -292,9 +319,13 @@ public class ArchitectureTest {
static final ArchRule everythingShouldBeFreeOfCycles =
slices().matching("net.hostsharing.hsadminng.(*)..")
.should().beFreeOfCycles()
// TODO.refa: would be great if we could get rid of these cyclic dependencies
.ignoreDependency(
ContextBasedTest.class,
net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.class);
RbacGrantsDiagramService.class)
.ignoreDependency(
HsBookingItemEntity.class,
HsHostingAssetEntity.class);
@ArchTest

View File

@ -187,7 +187,7 @@ class RestResponseEntityExceptionHandlerUnitTest {
final var givenWebRequest = mock(WebRequest.class);
// when
final var errorResponse = exceptionHandler.handleIbanAndBicExceptions(givenException, givenWebRequest);
final var errorResponse = exceptionHandler.handleValidationExceptions(givenException, givenWebRequest);
// then
assertThat(errorResponse.getBody().getStatusCode()).isEqualTo(400);

View File

@ -0,0 +1,51 @@
package net.hostsharing.hsadminng.hash;
import org.junit.jupiter.api.Test;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.Algorithm.SHA512;
import static net.hostsharing.hsadminng.hash.LinuxEtcShadowHashGenerator.hash;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
class LinuxEtcShadowHashGeneratorUnitTest {
final String GIVEN_PASSWORD = "given password";
final String WRONG_PASSWORD = "wrong password";
final String GIVEN_SALT = "0123456789abcdef";
// generated via mkpasswd for plaintext password GIVEN_PASSWORD (see above)
final String GIVEN_SHA512_HASH = "$6$ooei1HK6JXVaI7KC$sY5d9fEOr36hjh4CYwIKLMfRKL1539bEmbVCZ.zPiH0sv7jJVnoIXb5YEefEtoSM2WWgDi9hr7vXRe3Nw8zJP/";
final String GIVEN_YESCRYPT_HASH = "$y$j9T$wgYACPmBXvlMg2MzeZA0p1$KXUzd28nG.67GhPnBZ3aZsNNA5bWFdL/dyG4wS0iRw7";
@Test
void verifiesPasswordAgainstSha512HashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_SHA512_HASH); // throws exception if wrong
}
@Test
void verifiesPasswordAgainstYescryptHashFromMkpasswd() {
hash(GIVEN_PASSWORD).verify(GIVEN_YESCRYPT_HASH); // throws exception if wrong
}
@Test
void verifiesHashedPasswordWithRandomSalt() {
final var hash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate();
hash(GIVEN_PASSWORD).verify(hash); // throws exception if wrong
}
@Test
void verifiesHashedPasswordWithGivenSalt() {
final var givenPasswordHash =hash(GIVEN_PASSWORD).using(SHA512).withSalt(GIVEN_SALT).generate();
hash(GIVEN_PASSWORD).verify(givenPasswordHash); // throws exception if wrong
}
@Test
void throwsExceptionForInvalidPassword() {
final var givenPasswordHash = hash(GIVEN_PASSWORD).using(SHA512).withRandomSalt().generate();
final var throwable = catchThrowable(() ->
hash(WRONG_PASSWORD).verify(givenPasswordHash) // throws exception if wrong);
);
assertThat(throwable).hasMessage("invalid password");
}
}

Some files were not shown because too many files have changed in this diff Show More