TP-202405-filtered_import #78
@ -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'
|
||||
|
199
doc/hs-hosting-asset-type-structure.md
Normal file
199
doc/hs-hosting-asset-type-structure.md
Normal 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.
|
288
doc/projects-booking-items-and-hosting-entities.md
Normal file
288
doc/projects-booking-items-and-hosting-entities.md
Normal 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
|
||||
```
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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()));
|
||||
};
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,9 @@
|
||||
package net.hostsharing.hsadminng.hs.booking.item;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Node {
|
||||
|
||||
String nodeName();
|
||||
List<String> edges();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
@ -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()))));
|
||||
}
|
||||
};
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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()));
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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..> ");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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]$");
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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]+$");
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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]$");
|
||||
}
|
||||
}
|
||||
|
@ -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]$");
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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`.
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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) + "]";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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 (':')");
|
||||
}
|
||||
}
|
||||
}
|
@ -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 + "'");
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 --------------------------------
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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'
|
@ -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'
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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; $$;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
--//
|
@ -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');
|
||||
--//
|
@ -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
|
||||
|
||||
```
|
@ -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$);
|
||||
--//
|
||||
|
@ -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;
|
||||
$$;
|
||||
--//
|
@ -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)
|
||||
);
|
||||
--//
|
||||
|
@ -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
|
||||
|
@ -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$);
|
||||
--//
|
||||
|
@ -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;
|
||||
$$;
|
||||
--//
|
@ -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
|
||||
|
||||
```
|
@ -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$);
|
||||
--//
|
||||
|
@ -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; $$;
|
||||
|
@ -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
|
||||
|
||||
```
|
@ -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
|
||||
|
||||
```
|
@ -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
|
||||
|
||||
```
|
@ -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
|
||||
|
@ -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$);
|
||||
--//
|
||||
|
||||
|
@ -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;
|
||||
$$;
|
||||
--//
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user