Merge remote-tracking branch 'origin/master' into TP-20240927-importfixes
This commit is contained in:
commit
98ad6edeae
6
.aliases
6
.aliases
@ -30,13 +30,13 @@ postgresAutodoc () {
|
|||||||
fi
|
fi
|
||||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||||
-m '(rbacobject|hs).*' \
|
-m '(rbacobject|hs).*' \
|
||||||
-l /usr/share/postgresql-autodoc -t neato &&
|
-l /usr/share/postgresql-autodoc -t neato &&
|
||||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \
|
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-hs.svg && \
|
||||||
echo "generated: $PWD/build/postgres-autodoc-hs.svg"
|
echo "generated: $PWD/build/postgres-autodoc-hs.svg"
|
||||||
|
|
||||||
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
postgresql_autodoc -d postgres -f build/postgres-autodoc -h localhost -u postgres --password=password \
|
||||||
-m '(global|rbac).*' \
|
-m '(global|rbac).*' \
|
||||||
-l /usr/share/postgresql-autodoc -t neato &&
|
-l /usr/share/postgresql-autodoc -t neato &&
|
||||||
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
|
dot -Tsvg build/postgres-autodoc.neato >build/postgres-autodoc-rbac.svg && \
|
||||||
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
|
echo "generated $PWD/build/postgres-autodoc-rbac.svg"
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ alias fp='grep -r '@Accepts' src | sed -e 's/^.*@/@/g' | sort -u | wc -l'
|
|||||||
|
|
||||||
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
|
alias gw-spotless='./gradlew spotlessApply -x pitest -x test -x :processResources'
|
||||||
alias gw-test='. .aliases; ./gradlew test'
|
alias gw-test='. .aliases; ./gradlew test'
|
||||||
alias gw-check='. .aliases; gw test importOfficeData check -x pitest -x :dependencyCheckAnalyze'
|
alias gw-check='. .aliases; gw test check -x pitest'
|
||||||
|
|
||||||
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
|
# etc/docker-compose.yml limits CPUs+MEM and includes a PostgreSQL config for analysing slow queries
|
||||||
alias gw-importOfficeData-in-docker-compose='
|
alias gw-importOfficeData-in-docker-compose='
|
||||||
|
52
Jenkinsfile
vendored
Normal file
52
Jenkinsfile
vendored
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
pipeline {
|
||||||
|
agent {
|
||||||
|
dockerfile {
|
||||||
|
filename 'etc/jenkinsAgent.Dockerfile'
|
||||||
|
// additionalBuildArgs ...
|
||||||
|
args '--network=bridge --user root -v $PWD:$PWD -v /var/run/docker.sock:/var/run/docker.sock --group-add 984'
|
||||||
|
reuseNode true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DOCKER_HOST = 'unix:///var/run/docker.sock'
|
||||||
|
HSADMINNG_POSTGRES_ADMIN_USERNAME = 'admin'
|
||||||
|
HSADMINNG_POSTGRES_RESTRICTED_USERNAME = 'restricted'
|
||||||
|
HSADMINNG_MIGRATION_DATA_PATH = 'migration'
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers {
|
||||||
|
pollSCM('H/1 * * * *')
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Checkout') {
|
||||||
|
steps {
|
||||||
|
checkout scm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage ('Compile & Test') {
|
||||||
|
steps {
|
||||||
|
sh './gradlew clean check --no-daemon -x pitest -x dependencyCheckAnalyze'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
// archive test results
|
||||||
|
junit 'build/test-results/test/*.xml'
|
||||||
|
|
||||||
|
// archive the JaCoCo coverage report in XML and HTML format
|
||||||
|
jacoco(
|
||||||
|
execPattern: 'build/jacoco/*.exec',
|
||||||
|
classPattern: 'build/classes/java/main',
|
||||||
|
sourcePattern: 'src/main/java'
|
||||||
|
)
|
||||||
|
|
||||||
|
// cleanup workspace
|
||||||
|
cleanWs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
39
README.md
39
README.md
@ -497,9 +497,19 @@ We'll see if this changes when the project progresses and more validations are a
|
|||||||
|
|
||||||
### OWASP Security Vulnerability Check
|
### OWASP Security Vulnerability Check
|
||||||
|
|
||||||
An OWASP security vulnerability is configured and can be utilized by running:
|
An OWASP security vulnerability is configured, but you need an API key.
|
||||||
|
Fetch it from https://nvd.nist.gov/developers/request-an-api-key.
|
||||||
|
|
||||||
|
Then add it to your `~/.gradle/gradle.properties` file:
|
||||||
|
|
||||||
|
```
|
||||||
|
OWASP_API_KEY=........-....-....-....-............
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can run the dependency vulnerability check:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
gw dependencyCheckUpdate
|
||||||
gw dependencyCheckAnalyze
|
gw dependencyCheckAnalyze
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -550,12 +560,37 @@ Dependency versions can be automatically upgraded to the latest available versio
|
|||||||
gw useLatestVersions
|
gw useLatestVersions
|
||||||
```
|
```
|
||||||
|
|
||||||
Afterwards, `gw check` is automatically started.
|
Afterward, `gw check` is automatically started.
|
||||||
Please only commit+push to master if the check run shows no errors.
|
Please only commit+push to master if the check run shows no errors.
|
||||||
|
|
||||||
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
|
More infos, e.g. on blacklists see on the [project's website](https://github.com/patrikerdes/gradle-use-latest-versions-plugin).
|
||||||
|
|
||||||
|
|
||||||
|
## Biggest Flaws in our Architecture
|
||||||
|
|
||||||
|
### The RBAC System is too Complicated
|
||||||
|
|
||||||
|
Now, where we have a better experience with what we really need from the RBAC system, we have learned
|
||||||
|
that and creates too many (grant- and role-) rows and too even tables which could be avoided completely.
|
||||||
|
|
||||||
|
The basic idea is always to always have a fixed set of ordered role-types which apply for all DB-tables under RBAC,
|
||||||
|
e.g. OWNER>ADMIN>AGENT\[>PROXY?\]>TENENT>REFERRER.
|
||||||
|
Grants between these for the same DB-row would be implicit by order comparision.
|
||||||
|
This way we would get rid of all explicit grants within the same DB-row
|
||||||
|
and would not need the `rbac.role` table anymore.
|
||||||
|
We would also reduce the depth of the expensive recursive CTE-query.
|
||||||
|
|
||||||
|
This has to be explored further.
|
||||||
|
For now, we just keep it in mind and
|
||||||
|
|
||||||
|
### The Mapper is Error-Prone
|
||||||
|
|
||||||
|
Where `org.modelmapper.ModelMapper` reduces bloat-code a lot and has some nice features about recursive data-structure mappings,
|
||||||
|
it often causes strange errors which are hard to fix.
|
||||||
|
E.g. the uuid of the target main object is often taken from an uuid of a sub-subject.
|
||||||
|
(For now, use `StrictMapper` to avoid this, for the case it happens.)
|
||||||
|
|
||||||
|
|
||||||
## How To ...
|
## How To ...
|
||||||
|
|
||||||
### How to Configure .pgpass for the Default PostgreSQL Database?
|
### How to Configure .pgpass for the Default PostgreSQL Database?
|
||||||
|
@ -1,36 +1,38 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
# waits for commits on any branch on origin, checks it out and builds it
|
||||||
|
|
||||||
# get the current branch name
|
. .aliases
|
||||||
BRANCH=$(git rev-parse --abbrev-ref HEAD)
|
|
||||||
|
|
||||||
while true; do
|
while true; do
|
||||||
|
git fetch origin >/dev/null
|
||||||
|
branch_with_new_commits=`git fetch origin >/dev/null; git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads | grep '\[behind' | cut -d' ' -f1 | head -n1`
|
||||||
|
|
||||||
# get the latest commit hashes from origin and local
|
if [ -n "$branch_with_new_commits" ]; then
|
||||||
git fetch origin
|
echo "checking out branch: $branch_with_new_commits"
|
||||||
LOCAL=$(git rev-parse HEAD)
|
if git show-ref --quiet --heads "$branch_with_new_commits"; then
|
||||||
REMOTE=$(git rev-parse origin/$BRANCH)
|
echo "Branch $branch_with_new_commits already exists. Checking it out and pulling latest changes."
|
||||||
|
git checkout "$branch_with_new_commits"
|
||||||
|
git pull origin "$branch_with_new_commits"
|
||||||
|
else
|
||||||
|
echo "Creating and checking out new branch: $branch_with_new_commits"
|
||||||
|
git checkout -b "$branch_with_new_commits" "origin/$branch_with_new_commits"
|
||||||
|
fi
|
||||||
|
|
||||||
# check if the local branch differs from the remote branch
|
echo "building ..."
|
||||||
if [ "$LOCAL" != "$REMOTE" ]; then
|
./gradlew gw clean test check -x pitest
|
||||||
echo "local $LOCAL differs from remote $REMOTE => pulling changes from origin"
|
fi
|
||||||
git pull origin $BRANCH
|
|
||||||
|
|
||||||
# run the command
|
# wait 10s with a little animation
|
||||||
echo "Running ./gradlew test"
|
echo -e -n "\r\033[K waiting for changes (/) ..."
|
||||||
source .aliases # only variables, aliases are not expanded in scripts
|
sleep 2
|
||||||
./gradlew test
|
echo -e -n "\r\033[K waiting for changes (-) ..."
|
||||||
fi
|
sleep 2
|
||||||
|
echo -e -n "\r\033[K waiting for changes (\) ..."
|
||||||
# wait 10s with a little animation
|
sleep 2
|
||||||
echo -e -n " waiting for changes (/) ..."
|
echo -e -n "\r\033[K waiting for changes (|) ..."
|
||||||
sleep 2
|
sleep 2
|
||||||
echo -e -n "\r\033[K waiting for changes (-) ..."
|
echo -e -n "\r\033[K waiting for changes ( ) ... "
|
||||||
sleep 2
|
sleep 2
|
||||||
echo -e -n "\r\033[K waiting for changes (\) ..."
|
echo -e -n "\r\033[K checking for changes"
|
||||||
sleep 2
|
|
||||||
echo -e -n "\r\033[K waiting for changes (|) ..."
|
|
||||||
sleep 2
|
|
||||||
echo -e -n "\r\033[K waiting for changes ( ) ... "
|
|
||||||
sleep 2
|
|
||||||
echo -e -n "\r\033[K"
|
|
||||||
done
|
done
|
||||||
|
|
||||||
|
52
build.gradle
52
build.gradle
@ -1,10 +1,10 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'org.springframework.boot' version '3.2.4'
|
id 'org.springframework.boot' version '3.3.4'
|
||||||
id 'io.spring.dependency-management' version '1.1.4'
|
id 'io.spring.dependency-management' version '1.1.6'
|
||||||
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
id 'io.openapiprocessor.openapi-processor' version '2023.2'
|
||||||
id 'com.github.jk1.dependency-license-report' version '2.6'
|
id 'com.github.jk1.dependency-license-report' version '2.9'
|
||||||
id "org.owasp.dependencycheck" version "9.0.10"
|
id "org.owasp.dependencycheck" version "10.0.4"
|
||||||
id "com.diffplug.spotless" version "6.25.0"
|
id "com.diffplug.spotless" version "6.25.0"
|
||||||
id 'jacoco'
|
id 'jacoco'
|
||||||
id 'info.solidsoft.pitest' version '1.15.0'
|
id 'info.solidsoft.pitest' version '1.15.0'
|
||||||
@ -58,19 +58,20 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.1'
|
implementation 'com.github.gavlyukovskiy:datasource-proxy-spring-boot-starter:1.9.2'
|
||||||
implementation 'org.springdoc:springdoc-openapi:2.4.0'
|
implementation 'org.springdoc:springdoc-openapi:2.6.0'
|
||||||
implementation 'org.postgresql:postgresql:42.7.3'
|
implementation 'org.postgresql:postgresql:42.7.4'
|
||||||
implementation 'org.liquibase:liquibase-core:4.27.0'
|
implementation 'org.liquibase:liquibase-core:4.29.2'
|
||||||
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.7.3'
|
implementation 'io.hypersistence:hypersistence-utils-hibernate-63:3.8.3'
|
||||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.0'
|
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.0'
|
||||||
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
|
||||||
implementation 'org.apache.commons:commons-text:1.11.0'
|
implementation 'org.apache.commons:commons-text:1.12.0'
|
||||||
implementation 'net.java.dev.jna:jna:5.8.0'
|
implementation 'net.java.dev.jna:jna:5.15.0'
|
||||||
implementation 'org.modelmapper:modelmapper:3.2.0'
|
implementation 'org.modelmapper:modelmapper:3.2.1'
|
||||||
implementation 'org.iban4j:iban4j:3.2.7-RELEASE'
|
implementation 'org.iban4j:iban4j:3.2.10-RELEASE'
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.4.0'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
|
||||||
implementation 'org.reflections:reflections:0.9.12'
|
implementation 'org.webjars:swagger-ui:5.17.14'
|
||||||
|
implementation 'org.reflections:reflections:0.10.2'
|
||||||
|
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
testCompileOnly 'org.projectlombok:lombok'
|
testCompileOnly 'org.projectlombok:lombok'
|
||||||
@ -85,9 +86,9 @@ dependencies {
|
|||||||
testImplementation 'org.testcontainers:junit-jupiter'
|
testImplementation 'org.testcontainers:junit-jupiter'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
testImplementation 'org.testcontainers:postgresql'
|
testImplementation 'org.testcontainers:postgresql'
|
||||||
testImplementation 'com.tngtech.archunit:archunit-junit5:1.2.1'
|
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
|
||||||
testImplementation 'io.rest-assured:spring-mock-mvc'
|
testImplementation 'io.rest-assured:spring-mock-mvc'
|
||||||
testImplementation 'org.hamcrest:hamcrest-core:2.2'
|
testImplementation 'org.hamcrest:hamcrest-core:3.0'
|
||||||
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
|
testImplementation 'org.pitest:pitest-junit5-plugin:1.2.1'
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
testImplementation 'org.junit.jupiter:junit-jupiter-api'
|
||||||
}
|
}
|
||||||
@ -276,7 +277,7 @@ jacocoTestCoverageVerification {
|
|||||||
violationRules {
|
violationRules {
|
||||||
rule {
|
rule {
|
||||||
limit {
|
limit {
|
||||||
minimum = 0.92
|
minimum = 0.80 // TODO.test: improve instruction coverage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -288,15 +289,20 @@ jacocoTestCoverageVerification {
|
|||||||
element = 'CLASS'
|
element = 'CLASS'
|
||||||
excludes = [
|
excludes = [
|
||||||
'net.hostsharing.hsadminng.**.generated.**',
|
'net.hostsharing.hsadminng.**.generated.**',
|
||||||
|
'net.hostsharing.hsadminng.rbac.test.dom.TestDomainEntity',
|
||||||
'net.hostsharing.hsadminng.HsadminNgApplication',
|
'net.hostsharing.hsadminng.HsadminNgApplication',
|
||||||
'net.hostsharing.hsadminng.ping.PingController',
|
'net.hostsharing.hsadminng.ping.PingController',
|
||||||
|
'net.hostsharing.hsadminng.rbac.generator.*',
|
||||||
|
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService',
|
||||||
|
'net.hostsharing.hsadminng.rbac.grant.RbacGrantsDiagramService.Node',
|
||||||
|
'net.hostsharing.hsadminng.**.*Repository',
|
||||||
'net.hostsharing.hsadminng.mapper.Mapper'
|
'net.hostsharing.hsadminng.mapper.Mapper'
|
||||||
]
|
]
|
||||||
|
|
||||||
limit {
|
limit {
|
||||||
counter = 'LINE'
|
counter = 'LINE'
|
||||||
value = 'COVEREDRATIO'
|
value = 'COVEREDRATIO'
|
||||||
minimum = 0.98
|
minimum = 0.75 // TODO.test: improve line coverage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rule {
|
rule {
|
||||||
@ -310,7 +316,7 @@ jacocoTestCoverageVerification {
|
|||||||
limit {
|
limit {
|
||||||
counter = 'BRANCH'
|
counter = 'BRANCH'
|
||||||
value = 'COVEREDRATIO'
|
value = 'COVEREDRATIO'
|
||||||
minimum = 1.00
|
minimum = 0.00 // TODO.test: improve branch coverage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -343,14 +349,14 @@ pitest {
|
|||||||
targetClasses = ['net.hostsharing.hsadminng.**']
|
targetClasses = ['net.hostsharing.hsadminng.**']
|
||||||
excludedClasses = [
|
excludedClasses = [
|
||||||
'net.hostsharing.hsadminng.config.**',
|
'net.hostsharing.hsadminng.config.**',
|
||||||
'net.hostsharing.hsadminng.**.*Controller',
|
// 'net.hostsharing.hsadminng.**.*Controller',
|
||||||
'net.hostsharing.hsadminng.**.generated.**'
|
'net.hostsharing.hsadminng.**.generated.**'
|
||||||
]
|
]
|
||||||
|
|
||||||
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
|
targetTests = ['net.hostsharing.hsadminng.**.*UnitTest', 'net.hostsharing.hsadminng.**.*RestTest']
|
||||||
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
|
excludedTestClasses = ['**AcceptanceTest*', '**IntegrationTest*']
|
||||||
|
|
||||||
pitestVersion = '1.15.3'
|
pitestVersion = '1.17.0'
|
||||||
junit5PluginVersion = '1.1.0'
|
junit5PluginVersion = '1.1.0'
|
||||||
|
|
||||||
threads = 4
|
threads = 4
|
||||||
|
@ -0,0 +1,119 @@
|
|||||||
|
# Handling Automatic Creation of Hosting Assets for New Booking Items
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- [x] proposed by (Michael Hönnig)
|
||||||
|
- [ ] accepted by (Participants)
|
||||||
|
- [ ] rejected by (Participants)
|
||||||
|
- [ ] superseded by (superseding ADR)
|
||||||
|
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
When a customer creates a new booking item (e.g., `MANAGED_WEBSPACE`), the system must automatically create the related hosting asset.
|
||||||
|
This process can sometimes fail or require additional data from the user, e.g. installing a DNS verification key, or a hostmaster, e.g. the target server to use.
|
||||||
|
|
||||||
|
The challenge is how to handle this automatic creation process while dealing with missing data, asynchronicity and failures while ensuring system consistency and proper user notification.
|
||||||
|
|
||||||
|
|
||||||
|
### Technical Background
|
||||||
|
|
||||||
|
The creation of hosting assets can occur synchronously (in simple cases) or asynchronously (when additional steps like manual verification are needed).
|
||||||
|
For example, a `DOMAIN_SETUP` hosting asset may require DNS verification from the user, and until this is provided, the related domain cannot be fully set up.
|
||||||
|
|
||||||
|
Additionally, not all data needed for creating the hosting asset is stored in the booking item.
|
||||||
|
It's part of the HTTP request and later stored in the hosting asset, but we also need to store it before the hosting asset can be created asynchronously.
|
||||||
|
|
||||||
|
Current system behavior involves returning HTTP 201 upon booking item creation, but the automatic hosting asset creation might fail due to missing information.
|
||||||
|
The system needs to manage the creation process in a way that ensures valid hosting assets are created and informs the user of any actions required while still returning a 201 HTTP code, not an error code.
|
||||||
|
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
For storing the data needed for the hosting-asset creation:
|
||||||
|
|
||||||
|
* STORAGE-1: Store temporary asset data in the `BookingItemEntity`, e.g. a JSON column.
|
||||||
|
And delete the value of that column, once the hosting assets got successfully created.
|
||||||
|
* STORAGE-2: Create hosting assets immediately, even if invalid, but mark them as "inactive" until completed and fully validated.
|
||||||
|
* STORAGE-3: Store the asset data in a kind of event- or job-queue, which get deleted once the hosting-asset got successfully created.
|
||||||
|
|
||||||
|
For the user-notification status:
|
||||||
|
|
||||||
|
* STATUS-1: Introduce a status field in the booking-items.
|
||||||
|
* STATUS-2: Store the status in the event-/job-queue entries.
|
||||||
|
|
||||||
|
### STORAGE-1: Temporary Data Storage in `BookingItemEntity`
|
||||||
|
|
||||||
|
Store asset-related data (e.g., domain name) in a temporary column or JSON field in the `BookingItemEntity` until the hosting assets are successfully created.
|
||||||
|
Once assets are created, the temporary data is deleted to avoid inconsistencies.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Easy to implement.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Needs either a separate map of properties in the booking-item.
|
||||||
|
- Or, if stored as a JSON field in the booking-item-resources, these are misused.
|
||||||
|
- Requires additional cleanup logic to remove stale data.
|
||||||
|
|
||||||
|
### STORAGE-2: Inactive Hosting Assets Until Validation
|
||||||
|
|
||||||
|
Create the hosting assets immediately upon booking item creation but mark them as "inactive" until all required information (e.g., verification code) is provided and validation is complete.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Avoids temporary external data storage for the hosting-assets.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Validation becomes more complex as some properties need to be validated, others not.
|
||||||
|
And some properties even need special treatment for new entities, which then becomes vague.
|
||||||
|
- Inactive assets have to be filtered from operational assets.
|
||||||
|
- Potential risk of incomplete or inconsistent assets being created, which may require correction.
|
||||||
|
- Difficult to write tests for all possible combinations of validations.
|
||||||
|
|
||||||
|
### STORAGE-3: Event-Based Approach
|
||||||
|
|
||||||
|
The hosting asset data required for creation us passed to the API and stored in a `BookingItemCreatedEvent`.
|
||||||
|
If hosting asset creation cannot happen synchronously, the event is stored and processed asynchronously in batches, retrying failed asset creation as needed.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Clean-data-structure (separation of concerns).
|
||||||
|
- Clear separation between booking item creation and hosting asset creation.
|
||||||
|
- Only valid assets in the database.
|
||||||
|
- Can handle complex asynchronous processes (like waiting for external verification) in a clean and structured manner.
|
||||||
|
- Easier to manage retries and failures in asset creation without complicating the booking item structure.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- At the Spring controller level, the whole JSON is already converted into Java objects,
|
||||||
|
but for storing the asset data in the even, we need JSON again.
|
||||||
|
This could is not just a performance-overhead but could also lead to inconsistencies.
|
||||||
|
|
||||||
|
### STATUS-1: Store hosting-asset-creation-status in the `BookingItemEntity`
|
||||||
|
|
||||||
|
A status field would be added to booking-items to track the creation state of related hosting assets.
|
||||||
|
The users could check their booking-items for the status of the hosting-asset creation, error messages and further instructions.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Easy to implement.
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Adds a field to the booking-item which is makes no sense anymore once the related hosting asset is created.
|
||||||
|
|
||||||
|
|
||||||
|
### Status-2: Store hosting-asset-creation-status in the `BookingItemCreateEvent`
|
||||||
|
|
||||||
|
A status field would be added to the booking-item-created event and get updated with the latest messages any time we try to create the hosting-asset.
|
||||||
|
|
||||||
|
#### Advantages
|
||||||
|
- Clean-data-structure (separation of concerns)
|
||||||
|
|
||||||
|
#### Disadvantages
|
||||||
|
- Accessing the status requires querying the event queue.
|
||||||
|
|
||||||
|
|
||||||
|
## Decision Outcome
|
||||||
|
|
||||||
|
**Chosen Option: STORAGE-3 with STATUS-2 (Event-Based Approach with `BookingItemCreatedEvent`)**
|
||||||
|
|
||||||
|
The event-based approach was selected as the best solution for handling automatic hosting asset creation. This option provides a clear separation between booking item creation and hosting asset creation, ensuring that no invalid or incomplete assets are created. The asynchronous nature of the event system allows for retries and external validation steps (such as user-entered verification codes) without disrupting the overall flow.
|
||||||
|
|
||||||
|
By using `BookingItemCreatedEvent` to store the hosting-asset data and the status,
|
||||||
|
we don't need to misuse other data structures for temporary data
|
||||||
|
and therefore hava a clean separation of concerns.
|
@ -199,7 +199,7 @@ Limit (cost=6549.08..6549.35 rows=54 width=16)
|
|||||||
Group Key: grants.descendantuuid
|
Group Key: grants.descendantuuid
|
||||||
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
|
-> CTE Scan on grants (cost=0.00..22.06 rows=1103 width=16)
|
||||||
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
|
-> Index Only Scan using rbacobject_objecttable_uuid_key on rbacobject obj (cost=0.28..0.31 rows=1 width=16)
|
||||||
Index Cond: ((objecttable = 'hs_hosting_asset'::text) AND (uuid = perm.objectuuid))
|
Index Cond: ((objecttable = 'hs_hosting.asset'::text) AND (uuid = perm.objectuuid))
|
||||||
```
|
```
|
||||||
|
|
||||||
### Office-Relation-Query
|
### Office-Relation-Query
|
||||||
@ -276,15 +276,15 @@ At this point, the import took 21mins with these statistics:
|
|||||||
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||||
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||||
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||||
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||||
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||||
| insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
|
| insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 9 |
|
||||||
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
|
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 9 |
|
||||||
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
|
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 8 |
|
||||||
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
|
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47540 | 0 | 0 |
|
||||||
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
|
| insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing" | 40472 | 0 | 0 |
|
||||||
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||||
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||||
|
|
||||||
|
|
||||||
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
|
The slowest query now was fetching Relations joined with Contact, Anchor-Person and Holder-Person, for all tables using the restricted (RBAC) views (_rv).
|
||||||
@ -300,14 +300,14 @@ We changed these mappings from `EAGER` (default) to `LAZY` to `@ManyToOne(fetch
|
|||||||
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
| call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) | 31316 | 0 | 1 |
|
||||||
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
| select * from rbac.isGranted(array[granteeId], grantedId) | 44613 | 0 | 0 |
|
||||||
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
| call buildRbacSystemForHsHostingAsset(NEW) | 2258 | 0 | 7 |
|
||||||
| insert into public.hs_hosting_asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
| insert into public.hs_hosting.asset_rv (alarmcontactuuid,assignedtoassetuuid,bookingitemuuid,caption,config,identifier,parentassetuuid,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10) | 2207 | 0 | 7 |
|
||||||
| insert into hs_hosting_asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
| insert into hs_hosting.asset (alarmcontactuuid, version, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, config, uuid, identifier, caption) values (new.alarmcontactuuid, new. version, new. bookingitemuuid, new. type, new. parentassetuuid, new. assignedtoassetuuid, new. config, new. uuid, new. identifier, new. caption) returning * | 2207 | 0 | 7 |
|
||||||
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
|
| with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select ""grant"".descendantUuid, ""grant"".ascendantUuid from RbacGrants ""grant"" inner join grants recur on recur.ascendantUuid = ""grant"".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) | 47538 | 0 | 0 |
|
||||||
insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
|
insert into public.hs_office.relation_rv (anchoruuid,contactuuid,holderuuid,mark,type,version,uuid) values ($1,$2,$3,$4,$5,$6,$7) | 1261 | 0 | 8 |
|
||||||
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
|
| insert into hs_office.relation (uuid, version, anchoruuid, holderuuid, contactuuid, type, mark) values (new.uuid, new. version, new. anchoruuid, new. holderuuid, new. contactuuid, new. type, new. mark) returning * | 1261 | 0 | 8 |
|
||||||
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
|
| call buildRbacSystemForHsOfficeRelation(NEW) | 1276 | 0 | 7 |
|
||||||
| insert into public.hs_booking_item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
| insert into public.hs_booking.item_rv (caption,parentitemuuid,projectuuid,resources,type,validity,version,uuid) values ($1,$2,$3,$4,$5,$6,$7,$8) | 926 | 0 | 7 |
|
||||||
| insert into hs_booking_item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
| insert into hs_booking.item (resources, version, projectuuid, type, parentitemuuid, validity, uuid, caption) values (new.resources, new. version, new. projectuuid, new. type, new. parentitemuuid, new. validity, new. uuid, new. caption) returning * | 926 | 0 | 7 |
|
||||||
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
|
insert into RbacGrants (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed) values (currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume) on conflict do nothing | 40472 | 0 | 0 |
|
||||||
|
|
||||||
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
|
Now, finally, the total runtime of the import was down to 12 minutes. This is repeatable, where originally, the import took about 25mins in most cases and just rarely - and for unknown reasons - 10min.
|
||||||
@ -318,7 +318,7 @@ But once UnixUser and EmailAlias assets got added to the import, the total time
|
|||||||
|
|
||||||
This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours.
|
This was not acceptable, especially not, considering that domains, email-addresses and database-assets are almost 10 times that number and thus the import would go up to over 1100min which is 20 hours.
|
||||||
|
|
||||||
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting_asset) not to the RBAC-view (hs_hosting_asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
|
In a first step, a `HsHostingAssetRawEntity` was created, mapped to the raw table (hs_hosting.asset) not to the RBAC-view (hs_hosting.asset_rv). Unfortunately we did not keep measurements, but that was only part of the problem anyway.
|
||||||
|
|
||||||
The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened.
|
The main problem was, that there is something strange with persisting (`EntityManager.persist`) for EmailAlias assets. Where importing UnixUsers was mostly slow due to RBAC SELECT-permission checks, persisting EmailAliases suddenly created about a million (in numbers 1.000.000) SQL UPDATE statements after the INSERT, all with the same data, just increased version number (used for optimistic locking). We were not able to figure out why this happened.
|
||||||
|
|
||||||
@ -330,7 +330,7 @@ Now, the longest running queries are these:
|
|||||||
|
|
||||||
| No.| calls | total_m | mean_ms | query |
|
| No.| calls | total_m | mean_ms | query |
|
||||||
|---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|---:|---------|--------:|--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| 1 | 13.093 | 4 | 21 | insert into hs_hosting_asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
|
| 1 | 13.093 | 4 | 21 | insert into hs_hosting.asset( uuid, type, bookingitemuuid, parentassetuuid, assignedtoassetuuid, alarmcontactuuid, identifier, caption, config, version) values ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as jsonb), $10) |
|
||||||
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 |
|
| 2 | 517 | 4 | 502 | select hore1_0.uuid,hore1_0.anchoruuid,hore1_0.contactuuid,hore1_0.holderuuid,hore1_0.mark,hore1_0.type,hore1_0.version from public.hs_office.relation_rv hore1_0 where hore1_0.uuid=$1 |
|
||||||
| 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) |
|
| 3 | 13.144 | 4 | 21 | call buildRbacSystemForHsHostingAsset(NEW) |
|
||||||
| 4 | 96.632 | 3 | 2 | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) |
|
| 4 | 96.632 | 3 | 2 | call rbac.grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed) |
|
||||||
@ -338,10 +338,10 @@ Now, the longest running queries are these:
|
|||||||
| 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) |
|
| 6 | 123.740 | 3 | 2 | with recursive grants as ( select descendantUuid, ascendantUuid from RbacGrants where descendantUuid = grantedId union all select "grant".descendantUuid, "grant".ascendantUuid from RbacGrants "grant" inner join grants recur on recur.ascendantUuid = "grant".descendantUuid ) select exists ( select $3 from grants where ascendantUuid = any(granteeIds) ) or grantedId = any(granteeIds) |
|
||||||
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 |
|
| 7 | 497 | 2 | 259 | select hoce1_0.uuid,hoce1_0.caption,hoce1_0.emailaddresses,hoce1_0.phonenumbers,hoce1_0.postaladdress,hoce1_0.version from public.hs_office.contact_rv hoce1_0 where hoce1_0.uuid=$1 |
|
||||||
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 |
|
| 8 | 497 | 2 | 255 | select hope1_0.uuid,hope1_0.familyname,hope1_0.givenname,hope1_0.persontype,hope1_0.salutation,hope1_0.title,hope1_0.tradename,hope1_0.version from public.hs_office.person_rv hope1_0 where hope1_0.uuid=$1 |
|
||||||
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hsHostingAssetTENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hsHostingAssetAGENT(NEW), hsOfficeContactADMIN(newAlarmContact)], outgoingSubRoles => array[ hsBookingItemTENANT(newBookingItem), hsHostingAssetTENANT(newParentAsset)] ) |
|
| 9 | 13.144 | 1 | 8 | SELECT createRoleWithGrants( hs_hosting.asset_TENANT(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_hosting.asset_AGENT(NEW), hs_office.contact_ADMIN(newAlarmContact)], outgoingSubRoles => array[ hs_booking.item_TENANT(newBookingItem), hs_hosting.asset_TENANT(newParentAsset)] ) |
|
||||||
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hsHostingAssetADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hsBookingItemAGENT(newBookingItem), hsHostingAssetAGENT(newParentAsset), hsHostingAssetOWNER(NEW)] ) |
|
| 10 | 13.144 | 1 | 5 | SELECT createRoleWithGrants( hs_hosting.asset_ADMIN(NEW), permissions => array[$7], incomingSuperRoles => array[ hs_booking.item_AGENT(newBookingItem), hs_hosting.asset_AGENT(newParentAsset), hs_hosting.asset_OWNER(NEW)] ) |
|
||||||
|
|
||||||
That the `INSERT into hs_hosting_asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
|
That the `INSERT into hs_hosting.asset` (No. 1) takes up the most time, seems to be normal, and 21ms for each call is also fine.
|
||||||
|
|
||||||
It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time.
|
It seems that the trigger effects (eg. No. 3 and No. 4) are included in the measure for the causing INSERT, otherwise summing up the totals would exceed the actual total time of the whole import. And it was to be expected that building the RBAC rules for new business objects takes most of the time.
|
||||||
|
|
||||||
@ -408,12 +408,12 @@ We found some solution approaches:
|
|||||||
This optimization idea came from Michael Hierweck and was promising.
|
This optimization idea came from Michael Hierweck and was promising.
|
||||||
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
|
The idea is to reduce the size of the result of the recursive CTE query and maybe even speed up that query itself.
|
||||||
|
|
||||||
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting_asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
|
To evaluate this, I added a type column to the `rbacObject` table, initially as an enum hsHostingAssetType. Then I entered the type there for all rows from hs_hosting.asset. This means that 83,886 of 92,545 rows in `rbacobject` have a type set, leaving 8,659 without.
|
||||||
|
|
||||||
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
|
If we do this for other types (we currently have 1,271 relations and 927 booking items), it gets more complicated because they are different enum types. As varchar(16), we could lose performance again due to the higher storage space requirements.
|
||||||
|
|
||||||
But the performance gained is not particularly high anyway.
|
But the performance gained is not particularly high anyway.
|
||||||
See the average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN',
|
See the average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN',
|
||||||
joined with business query for all `'EMAIL_ADDRESSES'`:
|
joined with business query for all `'EMAIL_ADDRESSES'`:
|
||||||
|
|
||||||
| | D-1000000-hsh | D-1000300-mih |
|
| | D-1000000-hsh | D-1000300-mih |
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"allowedLicenses": [
|
"allowedLicenses": [
|
||||||
{ "moduleLicense": "Apache 2.0" },
|
|
||||||
{ "moduleLicense": "Apache 2" },
|
{ "moduleLicense": "Apache 2" },
|
||||||
|
{ "moduleLicense": "Apache 2.0" },
|
||||||
|
{ "moduleLicense": "Apache-2.0" },
|
||||||
{ "moduleLicense": "Apache License 2.0" },
|
{ "moduleLicense": "Apache License 2.0" },
|
||||||
|
{ "moduleLicense": "Apache License v2.0" },
|
||||||
{ "moduleLicense": "Apache License, Version 2.0" },
|
{ "moduleLicense": "Apache License, Version 2.0" },
|
||||||
{ "moduleLicense": "The Apache Software License, Version 2.0" },
|
{ "moduleLicense": "The Apache Software License, Version 2.0" },
|
||||||
|
|
||||||
@ -11,6 +13,8 @@
|
|||||||
{ "moduleLicense": "BSD-3-Clause" },
|
{ "moduleLicense": "BSD-3-Clause" },
|
||||||
{ "moduleLicense": "The BSD License" },
|
{ "moduleLicense": "The BSD License" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "The New BSD License" },
|
||||||
|
|
||||||
{ "moduleLicense": "CDDL 1.1" },
|
{ "moduleLicense": "CDDL 1.1" },
|
||||||
{ "moduleLicense": "CDDL/GPLv2+CE" },
|
{ "moduleLicense": "CDDL/GPLv2+CE" },
|
||||||
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
|
{ "moduleLicense": "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0" },
|
||||||
@ -29,11 +33,22 @@
|
|||||||
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
|
{ "moduleLicense": "GNU General Public License, version 2 with the GNU Classpath Exception" },
|
||||||
{ "moduleLicense": "GPL2 w/ CPE" },
|
{ "moduleLicense": "GPL2 w/ CPE" },
|
||||||
|
|
||||||
|
{ "moduleLicense": "LGPL, version 2.1"},
|
||||||
|
{ "moduleLicense": "LGPL-2.1-or-later"},
|
||||||
|
|
||||||
{ "moduleLicense": "MIT License" },
|
{ "moduleLicense": "MIT License" },
|
||||||
{ "moduleLicense": "MIT" },
|
{ "moduleLicense": "MIT" },
|
||||||
{ "moduleLicense": "The MIT License (MIT)" },
|
{ "moduleLicense": "The MIT License (MIT)" },
|
||||||
{ "moduleLicense": "The MIT License" },
|
{ "moduleLicense": "The MIT License" },
|
||||||
|
|
||||||
{ "moduleName": "org.springdoc:springdoc-openapi" }
|
{ "moduleLicense": "WTFPL" },
|
||||||
|
|
||||||
|
{
|
||||||
|
"moduleLicense": null,
|
||||||
|
"#moduleLicense": "Apache License 2.0, see https://github.com/springdoc/springdoc-openapi/blob/main/LICENSE",
|
||||||
|
"moduleVersion": "2.4.0",
|
||||||
|
"moduleName": "org.springdoc:springdoc-openapi"
|
||||||
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
10
etc/jenkinsAgent.Dockerfile
Normal file
10
etc/jenkinsAgent.Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM eclipse-temurin:21-jdk
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get install -y bind9-utils && \
|
||||||
|
apt-get clean && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
|
||||||
|
# RUN mkdir /opt/app
|
||||||
|
# COPY japp.jar /opt
|
||||||
|
# CMD ["java", "-jar", "/opt/app/japp.jar"]
|
@ -1,12 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
||||||
<suppress>
|
|
||||||
<notes><![CDATA[
|
|
||||||
Cyclic references are not possible if file comes in JSON text format.
|
|
||||||
]]></notes>
|
|
||||||
<packageUrl regex="true">^pkg:maven/com\.fasterxml\.jackson\.core/jackson\-databind@.*$</packageUrl>
|
|
||||||
<cpe>cpe:/a:fasterxml:jackson-databind</cpe>
|
|
||||||
</suppress>
|
|
||||||
<suppress>
|
<suppress>
|
||||||
<notes><![CDATA[
|
<notes><![CDATA[
|
||||||
Internal tooling, not exposed to the Internet.
|
Internal tooling, not exposed to the Internet.
|
||||||
@ -14,4 +7,10 @@
|
|||||||
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
<packageUrl regex="true">^pkg:maven/org\.pitest/pitest\-command\-line@.*$</packageUrl>
|
||||||
<cpe>cpe:/a:line:line</cpe>
|
<cpe>cpe:/a:line:line</cpe>
|
||||||
</suppress>
|
</suppress>
|
||||||
|
<suppress>
|
||||||
|
<notes><![CDATA[
|
||||||
|
Malicious HTTP redirect in JAXB on a REST-endpoint is not that dangerous.
|
||||||
|
]]></notes>
|
||||||
|
<cve>CVE-2024-9329</cve>
|
||||||
|
</suppress>
|
||||||
</suppressions>
|
</suppressions>
|
||||||
|
@ -6,21 +6,21 @@
|
|||||||
rollback;
|
rollback;
|
||||||
begin transaction;
|
begin transaction;
|
||||||
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
call defineContext('historization testing', null, 'superuser-alex@hostsharing.net',
|
||||||
-- 'hs_booking_project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
-- 'hs_booking.project#D-1000000-hshdefaultproject:ADMIN'); -- prod+test
|
||||||
'hs_booking_project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
|
'hs_booking.project#D-1000313-D-1000313defaultproject:ADMIN'); -- prod+test
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN'); -- prod
|
||||||
-- 'hs_booking_project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
-- 'hs_booking.project#D-1000300-mimdefaultproject:ADMIN'); -- test
|
||||||
-- update hs_hosting_asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
-- update hs_hosting.asset set caption='lug00 b' where identifier = 'lug00' and type = 'MANAGED_WEBSPACE'; -- prod
|
||||||
-- update hs_hosting_asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
-- update hs_hosting.asset set caption='hsh00 A ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||||
-- update hs_hosting_asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
-- update hs_hosting.asset set caption='hsh00 B ' || now()::text where identifier = 'hsh00' and type = 'MANAGED_WEBSPACE'; -- test
|
||||||
|
|
||||||
-- insert into hs_hosting_asset
|
-- insert into hs_hosting.asset
|
||||||
-- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid)
|
-- (uuid, bookingitemuuid, type, parentassetuuid, assignedtoassetuuid, identifier, caption, config, alarmcontactuuid)
|
||||||
-- values
|
-- values
|
||||||
-- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null,
|
-- (uuid_generate_v4(), null, 'EMAIL_ADDRESS', 'bbda5895-0569-4e20-bb4c-34f3a38f3f63'::uuid, null,
|
||||||
-- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null);
|
-- 'new@thi.example.org', 'some new E-Mail-Address', '{}'::jsonb, null);
|
||||||
|
|
||||||
delete from hs_hosting_asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
|
delete from hs_hosting.asset where uuid='5aea68d2-3b55-464f-8362-b05c76c5a681'::uuid;
|
||||||
commit;
|
commit;
|
||||||
|
|
||||||
-- single version at point in time
|
-- single version at point in time
|
||||||
@ -29,11 +29,11 @@ set hsadminng.tx_history_txid to '';
|
|||||||
set hsadminng.tx_history_timestamp to '2024-08-29 12:42';
|
set hsadminng.tx_history_timestamp to '2024-08-29 12:42';
|
||||||
-- all versions
|
-- all versions
|
||||||
select base.tx_history_txid(), txc.txtimestamp, txc.currentSubject, txc.currentTask, haex.*
|
select base.tx_history_txid(), txc.txtimestamp, txc.currentSubject, txc.currentTask, haex.*
|
||||||
from hs_hosting_asset_ex haex
|
from hs_hosting.asset_ex haex
|
||||||
join base.tx_context txc on haex.txid=txc.txid
|
join base.tx_context txc on haex.txid=txc.txid
|
||||||
where haex.identifier = 'test@thi.example.org';
|
where haex.identifier = 'test@thi.example.org';
|
||||||
|
|
||||||
select uuid, version, type, identifier, caption from hs_hosting_asset_hv p where identifier = 'test@thi.example.org';
|
select uuid, version, type, identifier, caption from hs_hosting.asset_hv p where identifier = 'test@thi.example.org';
|
||||||
|
|
||||||
select pg_current_xact_id();
|
select pg_current_xact_id();
|
||||||
|
|
||||||
|
@ -6,10 +6,10 @@ select * from hs_statistics_v;
|
|||||||
|
|
||||||
-- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table
|
-- This is the extracted recursive CTE query to determine the visible object UUIDs of a single table
|
||||||
-- (and optionally the hosting-asset-type) as a separate VIEW.
|
-- (and optionally the hosting-asset-type) as a separate VIEW.
|
||||||
-- In the generated code this is part of the hs_hosting_asset_rv VIEW.
|
-- In the generated code this is part of the hs_hosting.asset_rv VIEW.
|
||||||
|
|
||||||
drop view if exists hs_hosting_asset_example_gv;
|
drop view if exists hs_hosting.asset_example_gv;
|
||||||
create view hs_hosting_asset_example_gv as
|
create view hs_hosting.asset_example_gv as
|
||||||
with recursive
|
with recursive
|
||||||
recursive_grants as (
|
recursive_grants as (
|
||||||
select distinct rbacgrants.descendantuuid,
|
select distinct rbacgrants.descendantuuid,
|
||||||
@ -40,7 +40,7 @@ select distinct perm.objectuuid
|
|||||||
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
|
join rbacpermission perm on recursive_grants.descendantuuid = perm.uuid
|
||||||
join rbacobject obj on obj.uuid = perm.objectuuid
|
join rbacobject obj on obj.uuid = perm.objectuuid
|
||||||
join count_check cc on cc.valid
|
join count_check cc on cc.valid
|
||||||
where obj.objecttable::text = 'hs_hosting_asset'::text
|
where obj.objecttable::text = 'hs_hosting.asset'::text
|
||||||
-- with/without this type condition
|
-- with/without this type condition
|
||||||
-- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
-- and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||||
and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
and obj.type = 'EMAIL_ADDRESS'::hshostingassettype
|
||||||
@ -53,10 +53,10 @@ select distinct perm.objectuuid
|
|||||||
rollback transaction;
|
rollback transaction;
|
||||||
begin transaction;
|
begin transaction;
|
||||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
SET TRANSACTION READ ONLY;
|
SET TRANSACTION READ ONLY;
|
||||||
EXPLAIN ANALYZE select * from hs_hosting_asset_example_gv;
|
EXPLAIN ANALYZE select * from hs_hosting.asset_example_gv;
|
||||||
end transaction ;
|
end transaction ;
|
||||||
|
|
||||||
-- ========================================================
|
-- ========================================================
|
||||||
@ -64,15 +64,15 @@ end transaction ;
|
|||||||
-- An example for a restricted view (_rv) similar to the one generated by our RBAC system,
|
-- An example for a restricted view (_rv) similar to the one generated by our RBAC system,
|
||||||
-- but using the above separate VIEW to determine the visible objects.
|
-- but using the above separate VIEW to determine the visible objects.
|
||||||
|
|
||||||
drop view if exists hs_hosting_asset_example_rv;
|
drop view if exists hs_hosting.asset_example_rv;
|
||||||
create view hs_hosting_asset_example_rv as
|
create view hs_hosting.asset_example_rv as
|
||||||
with accessible_hs_hosting_asset_uuids as (
|
with accessible_hs_hosting.asset_uuids as (
|
||||||
select * from hs_hosting_asset_example_gv
|
select * from hs_hosting.asset_example_gv
|
||||||
)
|
)
|
||||||
select target.*
|
select target.*
|
||||||
from hs_hosting_asset target
|
from hs_hosting.asset target
|
||||||
where (target.uuid in (select accessible_hs_hosting_asset_uuids.objectuuid
|
where (target.uuid in (select accessible_hs_hosting.asset_uuids.objectuuid
|
||||||
from accessible_hs_hosting_asset_uuids));
|
from accessible_hs_hosting.asset_uuids));
|
||||||
|
|
||||||
-- -------------------------------------------------------------------------------
|
-- -------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -89,8 +89,8 @@ BEGIN
|
|||||||
start_time := clock_timestamp();
|
start_time := clock_timestamp();
|
||||||
|
|
||||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
SET TRANSACTION READ ONLY;
|
SET TRANSACTION READ ONLY;
|
||||||
|
|
||||||
FOR i IN 0..25 LOOP
|
FOR i IN 0..25 LOOP
|
||||||
@ -99,7 +99,7 @@ BEGIN
|
|||||||
|
|
||||||
-- An example for a business query based on the view:
|
-- An example for a business query based on the view:
|
||||||
select type, uuid, identifier, caption
|
select type, uuid, identifier, caption
|
||||||
from hs_hosting_asset_example_rv
|
from hs_hosting.asset_example_rv
|
||||||
where type = 'EMAIL_ADDRESS'
|
where type = 'EMAIL_ADDRESS'
|
||||||
and identifier like letter || '%'
|
and identifier like letter || '%'
|
||||||
-- end of the business query example.
|
-- end of the business query example.
|
||||||
@ -115,7 +115,7 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
-- average seconds per recursive CTE select as role 'hs_hosting_asset:<DEBITOR>defaultproject:ADMIN'
|
-- average seconds per recursive CTE select as role 'hs_hosting.asset:<DEBITOR>defaultproject:ADMIN'
|
||||||
-- joined with business query for all 'EMAIL_ADDRESSES':
|
-- joined with business query for all 'EMAIL_ADDRESSES':
|
||||||
-- D-1000000-hsh D-1000300-mih
|
-- D-1000000-hsh D-1000300-mih
|
||||||
-- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23
|
-- - without type comparison in rbacobject: ~3.30 - ~3.49 ~0.23
|
||||||
@ -128,15 +128,15 @@ $$;
|
|||||||
rollback transaction;
|
rollback transaction;
|
||||||
begin transaction;
|
begin transaction;
|
||||||
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
CALL defineContext('performance testing', null, 'superuser-alex@hostsharing.net',
|
||||||
'hs_booking_project#D-1000000-hshdefaultproject:ADMIN');
|
'hs_booking.project#D-1000000-hshdefaultproject:ADMIN');
|
||||||
-- 'hs_booking_project#D-1000300-mihdefaultproject:ADMIN');
|
-- 'hs_booking.project#D-1000300-mihdefaultproject:ADMIN');
|
||||||
SET TRANSACTION READ ONLY;
|
SET TRANSACTION READ ONLY;
|
||||||
|
|
||||||
EXPLAIN SELECT * from (
|
EXPLAIN SELECT * from (
|
||||||
|
|
||||||
-- An example for a business query based on the view:
|
-- An example for a business query based on the view:
|
||||||
select type, uuid, identifier, caption
|
select type, uuid, identifier, caption
|
||||||
from hs_hosting_asset_example_rv
|
from hs_hosting.asset_example_rv
|
||||||
where type = 'EMAIL_ADDRESS'
|
where type = 'EMAIL_ADDRESS'
|
||||||
-- and identifier like 'b%'
|
-- and identifier like 'b%'
|
||||||
-- end of the business query example.
|
-- end of the business query example.
|
||||||
@ -151,17 +151,17 @@ end transaction;
|
|||||||
|
|
||||||
alter table rbacobject
|
alter table rbacobject
|
||||||
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
|
-- just for performance testing, we would need a joined enum or a varchar(16) which would make it slow
|
||||||
add column type hshostingassettype;
|
add column type hs_hosting.AssetType;
|
||||||
|
|
||||||
-- and fill the type column with hs_hosting_asset types:
|
-- and fill the type column with hs_hosting.asset types:
|
||||||
|
|
||||||
rollback transaction;
|
rollback transaction;
|
||||||
begin transaction;
|
begin transaction;
|
||||||
call defineContext('setting rbacobject.type from hs_hosting_asset.type', null, 'superuser-alex@hostsharing.net');
|
call defineContext('setting rbacobject.type from hs_hosting.asset.type', null, 'superuser-alex@hostsharing.net');
|
||||||
|
|
||||||
UPDATE rbacobject
|
UPDATE rbacobject
|
||||||
SET type = hs.type
|
SET type = hs.type
|
||||||
FROM hs_hosting_asset hs
|
FROM hs_hosting.asset hs
|
||||||
WHERE rbacobject.uuid = hs.uuid;
|
WHERE rbacobject.uuid = hs.uuid;
|
||||||
|
|
||||||
end transaction;
|
end transaction;
|
||||||
|
@ -17,7 +17,7 @@ public class JsonObjectMapperConfiguration {
|
|||||||
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
public Jackson2ObjectMapperBuilder customObjectMapper() {
|
||||||
return new Jackson2ObjectMapperBuilder()
|
return new Jackson2ObjectMapperBuilder()
|
||||||
.modules(new JsonNullableModule(), new JavaTimeModule())
|
.modules(new JsonNullableModule(), new JavaTimeModule())
|
||||||
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS)
|
.featuresToEnable(JsonParser.Feature.ALLOW_COMMENTS, JsonParser.Feature.ALLOW_COMMENTS)
|
||||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,15 +9,25 @@ import java.lang.annotation.Target;
|
|||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
public @interface DisplayAs {
|
public @interface DisplayAs {
|
||||||
|
|
||||||
class DisplayName {
|
class DisplayName {
|
||||||
|
|
||||||
public static String of(final Class<?> clazz) {
|
public static String of(final Class<?> clazz) {
|
||||||
final var displayNameAnnot = clazz.getAnnotation(DisplayAs.class);
|
final var displayNameAnnot = getDisplayNameAnnotation(clazz);
|
||||||
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
|
return displayNameAnnot != null ? displayNameAnnot.value() : clazz.getSimpleName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String of(@NotNull final Object instance) {
|
public static String of(@NotNull final Object instance) {
|
||||||
return of(instance.getClass());
|
return of(instance.getClass());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static DisplayAs getDisplayNameAnnotation(final Class<?> clazz) {
|
||||||
|
if (clazz == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var annot = clazz.getAnnotation(DisplayAs.class);
|
||||||
|
return annot != null ? annot : getDisplayNameAnnotation(clazz.getSuperclass());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String value() default "";
|
String value() default "";
|
||||||
|
@ -27,7 +27,7 @@ public final class HashGenerator {
|
|||||||
"abcdefghijklmnopqrstuvwxyz" +
|
"abcdefghijklmnopqrstuvwxyz" +
|
||||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
|
||||||
"0123456789/.";
|
"0123456789/.";
|
||||||
private static boolean couldBeHashEnabled; // TODO.impl: remove after legacy data is migrated
|
private static boolean couldBeHashEnabled; // TODO.legacy: remove after legacy data is migrated
|
||||||
|
|
||||||
public enum Algorithm {
|
public enum Algorithm {
|
||||||
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
LINUX_SHA512(LinuxEtcShadowHashGenerator::hash, "6"),
|
||||||
|
@ -18,7 +18,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
|||||||
|
|
||||||
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
|
// a partial HsOfficeDebitorEntity to reduce the number of SQL queries to load the entity
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_booking_debitor_xv")
|
@Table(schema = "hs_booking", name = "debitor_xv")
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class BookingItemCreatedAppEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private BookingItemCreatedEventEntity entity;
|
||||||
|
|
||||||
|
public BookingItemCreatedAppEvent(
|
||||||
|
@NotNull final Object source,
|
||||||
|
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||||
|
final String assetJson) {
|
||||||
|
super(source);
|
||||||
|
this.entity = new BookingItemCreatedEventEntity(newBookingItem, assetJson);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.experimental.SuperBuilder;
|
||||||
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.MapsId;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(schema = "hs_booking", name = "item_created_event")
|
||||||
|
@SuperBuilder(toBuilder = true)
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class BookingItemCreatedEventEntity implements BaseEntity {
|
||||||
|
@Id
|
||||||
|
@Column(name="bookingitemuuid")
|
||||||
|
private UUID uuid;
|
||||||
|
|
||||||
|
@MapsId
|
||||||
|
@ManyToOne(optional = false)
|
||||||
|
@JoinColumn(name = "bookingitemuuid", nullable = false)
|
||||||
|
private HsBookingItemRealEntity bookingItem;
|
||||||
|
|
||||||
|
@Version
|
||||||
|
private int version;
|
||||||
|
|
||||||
|
@Column(name = "assetjson")
|
||||||
|
private String assetJson;
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
@Column(name = "statusmessage")
|
||||||
|
private String statusMessage;
|
||||||
|
|
||||||
|
public BookingItemCreatedEventEntity(
|
||||||
|
@NotNull final HsBookingItemRealEntity newBookingItem,
|
||||||
|
final String assetJson) {
|
||||||
|
this.bookingItem = newBookingItem;
|
||||||
|
this.assetJson = assetJson;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import org.springframework.data.repository.Repository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface BookingItemCreatedEventRepository extends Repository<BookingItemCreatedEventEntity, UUID> {
|
||||||
|
|
||||||
|
BookingItemCreatedEventEntity save(HsBookingItemRealEntity current);
|
||||||
|
|
||||||
|
BookingItemCreatedEventEntity findByBookingItem(HsBookingItemRealEntity newBookingItem);
|
||||||
|
}
|
@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
|||||||
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
|
@ -1,26 +1,31 @@
|
|||||||
package net.hostsharing.hsadminng.hs.booking.item;
|
package net.hostsharing.hsadminng.hs.booking.item;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingItemsApi;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemPatchResource;
|
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.generated.api.v1.model.HsBookingItemResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.validators.BookingItemEntitySaveProcessor;
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
import net.hostsharing.hsadminng.hs.booking.item.validators.HsBookingItemEntityValidatorRegistry;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.project.HsBookingProjectRealEntity;
|
||||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
|
||||||
import jakarta.persistence.PersistenceContext;
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.toPostgresDateRange;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -30,13 +35,19 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StrictMapper mapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ApplicationEventPublisher applicationEventPublisher;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsBookingItemRbacRepository bookingItemRepo;
|
private HsBookingItemRbacRepository bookingItemRepo;
|
||||||
|
|
||||||
@PersistenceContext
|
@Autowired
|
||||||
private EntityManager em;
|
private ObjectMapper jsonMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EntityManagerWrapper em;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@ -48,7 +59,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
|
|
||||||
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
final var entities = bookingItemRepo.findAllByProjectUuid(projectUuid);
|
||||||
|
|
||||||
final var resources = mapper.mapList(entities, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
final var resources = mapper.mapList(entities, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(resources);
|
return ResponseEntity.ok(resources);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,15 +73,22 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
context.define(currentSubject, assumedRoles);
|
context.define(currentSubject, assumedRoles);
|
||||||
|
|
||||||
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
final var entityToSave = mapper.map(body, HsBookingItemRbacEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
|
final var saveProcessor = new BookingItemEntitySaveProcessor(em, entityToSave);
|
||||||
final var saved = HsBookingItemEntityValidatorRegistry.validated(em, bookingItemRepo.save(entityToSave));
|
final var mapped = saveProcessor
|
||||||
|
.preprocessEntity()
|
||||||
|
.validateEntity()
|
||||||
|
.prepareForSave()
|
||||||
|
.save()
|
||||||
|
.validateContext()
|
||||||
|
.mapUsing(e -> mapper.map(e, HsBookingItemResource.class, ITEM_TO_RESOURCE_POSTMAPPER))
|
||||||
|
.revampProperties();
|
||||||
|
publishSavedEvent(saveProcessor, body);
|
||||||
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/booking/items/{id}")
|
.path("/api/hs/booking/items/{id}")
|
||||||
.buildAndExpand(saved.getUuid())
|
.buildAndExpand(mapped.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
|
||||||
return ResponseEntity.created(uri).body(mapped);
|
return ResponseEntity.created(uri).body(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +105,7 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
|
result.ifPresent(entity -> em.detach(entity)); // prevent further LAZY-loading
|
||||||
return result
|
return result
|
||||||
.map(bookingItemEntity -> ResponseEntity.ok(
|
.map(bookingItemEntity -> ResponseEntity.ok(
|
||||||
mapper.map(bookingItemEntity, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER)))
|
mapper.map(bookingItemEntity, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER)))
|
||||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,18 +138,34 @@ public class HsBookingItemController implements HsBookingItemsApi {
|
|||||||
new HsBookingItemEntityPatcher(current).apply(body);
|
new HsBookingItemEntityPatcher(current).apply(body);
|
||||||
|
|
||||||
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
final var saved = bookingItemRepo.save(HsBookingItemEntityValidatorRegistry.validated(em, current));
|
||||||
final var mapped = mapper.map(saved, HsBookingItemResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
final var mapped = mapper.map(saved, HsBookingItemResource.class, RBAC_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
private void publishSavedEvent(final BookingItemEntitySaveProcessor saveProcessor, final HsBookingItemInsertResource body) {
|
||||||
|
try {
|
||||||
|
final var bookingItemRealEntity = em.getReference(HsBookingItemRealEntity.class, saveProcessor.getEntity().getUuid());
|
||||||
|
applicationEventPublisher.publishEvent(new BookingItemCreatedAppEvent(
|
||||||
|
this, bookingItemRealEntity, jsonMapper.writeValueAsString(body.getHostingAsset())));
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final BiConsumer<HsBookingItem, HsBookingItemResource> ITEM_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
resource.setValidFrom(entity.getValidity().lower());
|
resource.setValidFrom(entity.getValidity().lower());
|
||||||
if (entity.getValidity().hasUpperBound()) {
|
if (entity.getValidity().hasUpperBound()) {
|
||||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
final BiConsumer<HsBookingItemRbacEntity, HsBookingItemResource> RBAC_ENTITY_TO_RESOURCE_POSTMAPPER = ITEM_TO_RESOURCE_POSTMAPPER::accept;
|
||||||
|
|
||||||
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
final BiConsumer<HsBookingItemInsertResource, HsBookingItemRbacEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
|
entity.setProject(em.find(HsBookingProjectRealEntity.class, resource.getProjectUuid()));
|
||||||
|
ofNullable(resource.getParentItemUuid())
|
||||||
|
.map(parentItemUuid -> em.find(HsBookingItemRealEntity.class, parentItemUuid))
|
||||||
|
.ifPresent(entity::setParentItem);
|
||||||
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
entity.setValidity(toPostgresDateRange(LocalDate.now(), resource.getValidTo()));
|
||||||
entity.putResources(KeyValueMap.from(resource.getResources()));
|
entity.putResources(KeyValueMap.from(resource.getResources()));
|
||||||
};
|
};
|
||||||
|
@ -31,7 +31,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetc
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_booking_item_rv")
|
@Table(schema = "hs_booking", name = "item_rv")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -13,7 +13,7 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_booking_item")
|
@Table(schema = "hs_booking", name = "item")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -0,0 +1,136 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingItemResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||||
|
import net.hostsharing.hsadminng.hs.validation.HsEntityValidator;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
// TODO.refa: introduce common base class with HsHostingAssetEntitySaveProcessor
|
||||||
|
/**
|
||||||
|
* Wraps the steps of the pararation, validation, mapping and revamp around saving of a HsBookingItem into a readable API.
|
||||||
|
*/
|
||||||
|
public class BookingItemEntitySaveProcessor {
|
||||||
|
|
||||||
|
private final HsEntityValidator<HsBookingItem> validator;
|
||||||
|
private String expectedStep = "preprocessEntity";
|
||||||
|
private final EntityManager em;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private HsBookingItem entity;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private HsBookingItemResource resource;
|
||||||
|
|
||||||
|
public BookingItemEntitySaveProcessor(final EntityManager em, final HsBookingItem entity) {
|
||||||
|
this.em = em;
|
||||||
|
this.entity = entity;
|
||||||
|
this.validator = HsBookingItemEntityValidatorRegistry.forType(entity.getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// initial step allowing to set default values before any validations
|
||||||
|
public BookingItemEntitySaveProcessor preprocessEntity() {
|
||||||
|
step("preprocessEntity", "validateEntity");
|
||||||
|
validator.preprocessEntity(entity);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validates the entity itself including its properties
|
||||||
|
public BookingItemEntitySaveProcessor validateEntity() {
|
||||||
|
step("validateEntity", "prepareForSave");
|
||||||
|
MultiValidationException.throwIfNotEmpty(validator.validateEntity(entity));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO.legacy: remove once the migration of legacy data is done
|
||||||
|
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||||
|
public BookingItemEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||||
|
step("validateEntity", "prepareForSave");
|
||||||
|
final var ignoreRegExpPatterns = Arrays.stream(ignoreRegExp).map(Pattern::compile).toList();
|
||||||
|
MultiValidationException.throwIfNotEmpty(
|
||||||
|
validator.validateEntity(entity).stream()
|
||||||
|
.filter(error -> ignoreRegExpPatterns.stream().noneMatch(p -> p.matcher(error).matches() ))
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// hashing passwords etc.
|
||||||
|
public BookingItemEntitySaveProcessor prepareForSave() {
|
||||||
|
step("prepareForSave", "save");
|
||||||
|
validator.prepareProperties(em, entity);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the entity using the given `saveFunction`.
|
||||||
|
*
|
||||||
|
* <p>`validator.postPersist(em, entity)` is NOT called.
|
||||||
|
* If any postprocessing is necessary, the saveFunction has to implement this.</p>
|
||||||
|
* @param saveFunction
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public BookingItemEntitySaveProcessor saveUsing(final Function<HsBookingItem, HsBookingItem> saveFunction) {
|
||||||
|
step("save", "validateContext");
|
||||||
|
entity = saveFunction.apply(entity);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the using the `EntityManager`, but does NOT ever merge the entity.
|
||||||
|
*
|
||||||
|
* <p>`validator.postPersist(em, entity)` is called afterwards with the entity guaranteed to be flushed to the database.</p>
|
||||||
|
* @return this
|
||||||
|
*/
|
||||||
|
public BookingItemEntitySaveProcessor save() {
|
||||||
|
return saveUsing(e -> {
|
||||||
|
if (!em.contains(entity)) {
|
||||||
|
em.persist(entity);
|
||||||
|
}
|
||||||
|
em.flush(); // makes RbacEntity available as RealEntity if needed
|
||||||
|
validator.postPersist(em, entity);
|
||||||
|
return entity;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// validates the entity within it's parent and child hierarchy (e.g. totals validators and other limits)
|
||||||
|
public BookingItemEntitySaveProcessor validateContext() {
|
||||||
|
step("validateContext", "mapUsing");
|
||||||
|
return HsEntityValidator.doWithEntityManager(em, () -> {
|
||||||
|
MultiValidationException.throwIfNotEmpty(validator.validateContext(entity));
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// maps entity to JSON resource representation
|
||||||
|
public BookingItemEntitySaveProcessor mapUsing(
|
||||||
|
final Function<HsBookingItem, HsBookingItemResource> mapFunction) {
|
||||||
|
step("mapUsing", "revampProperties");
|
||||||
|
resource = mapFunction.apply(entity);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// removes write-only-properties and ads computed-properties
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public HsBookingItemResource revampProperties() {
|
||||||
|
step("revampProperties", null);
|
||||||
|
final var revampedProps = validator.revampProperties(em, entity, (Map<String, Object>) resource.getResources());
|
||||||
|
resource.setResources(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;
|
||||||
|
}
|
||||||
|
}
|
@ -48,10 +48,11 @@ public class HsBookingItemEntityValidatorRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
public static List<String> doValidate(final EntityManager em, final HsBookingItem bookingItem) {
|
||||||
|
final var bookingItemValidator = HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType());
|
||||||
return HsEntityValidator.doWithEntityManager(em, () ->
|
return HsEntityValidator.doWithEntityManager(em, () ->
|
||||||
HsEntityValidator.sequentiallyValidate(
|
HsEntityValidator.sequentiallyValidate(
|
||||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateEntity(bookingItem),
|
() -> bookingItemValidator.validateEntity(bookingItem),
|
||||||
() -> HsBookingItemEntityValidatorRegistry.forType(bookingItem.getType()).validateContext(bookingItem))
|
() -> bookingItemValidator.validateContext(bookingItem))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
package net.hostsharing.hsadminng.hs.booking.item.validators;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItem;
|
||||||
@ -12,13 +13,16 @@ import static net.hostsharing.hsadminng.hs.hosting.asset.validators.Dns.REGISTRA
|
|||||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
||||||
|
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
||||||
|
|
||||||
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
public static final String FQDN_REGEX = "^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,12}";
|
||||||
public static final String DOMAIN_NAME_PROPERTY_NAME = "domainName";
|
public static final String WEBSPACE_NAME_REGEX = "[a-z][a-z0-9]{2}[0-9]{2}";
|
||||||
|
public static final String TARGET_UNIX_USER_NAME_REGEX = "^"+WEBSPACE_NAME_REGEX+"$|^"+WEBSPACE_NAME_REGEX+"-[a-z0-9\\._-]+$";
|
||||||
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
public static final String VERIFICATION_CODE_PROPERTY_NAME = "verificationCode";
|
||||||
|
|
||||||
HsDomainSetupBookingItemValidator() {
|
HsDomainSetupBookingItemValidator() {
|
||||||
super(
|
super(
|
||||||
|
// TODO.spec: feels wrong
|
||||||
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
stringProperty(DOMAIN_NAME_PROPERTY_NAME).writeOnce()
|
||||||
.maxLength(253)
|
.maxLength(253)
|
||||||
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
.matchesRegEx(FQDN_REGEX).describedAs("is not a (non-top-level) fully qualified domain name")
|
||||||
@ -45,6 +49,11 @@ class HsDomainSetupBookingItemValidator extends HsBookingItemEntityValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
private static String generateVerificationCode(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||||
|
final var userDefinedVerificationCode = propertiesProvider.getDirectValue(VERIFICATION_CODE_PROPERTY_NAME, String.class);
|
||||||
|
if (userDefinedVerificationCode != null) {
|
||||||
|
return userDefinedVerificationCode;
|
||||||
|
}
|
||||||
|
|
||||||
final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
final var alphaNumeric = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||||
final var secureRandom = new SecureRandom();
|
final var secureRandom = new SecureRandom();
|
||||||
final var sb = new StringBuilder();
|
final var sb = new StringBuilder();
|
||||||
|
@ -3,30 +3,14 @@ package net.hostsharing.hsadminng.hs.booking.project;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
import net.hostsharing.hsadminng.hs.booking.debitor.HsBookingDebitorEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Column.dependsOnColumn;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingCase;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.ColumnValue.usingDefaultCase;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.GLOBAL;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Nullable.NOT_NULL;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Permission.*;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql;
|
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
|
||||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
@ -66,50 +50,4 @@ public abstract class HsBookingProject implements Stringifyable, BaseEntity<HsBo
|
|||||||
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
|
return ofNullable(debitor).map(HsBookingDebitorEntity::toShortString).orElse("D-???????") +
|
||||||
":" + caption;
|
":" + caption;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RbacView rbac() {
|
|
||||||
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
|
||||||
.withIdentityView(SQL.query("""
|
|
||||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.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", HsOfficeRelationRbacEntity.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).unassumed();
|
|
||||||
})
|
|
||||||
.createSubRole(ADMIN, (with) -> {
|
|
||||||
with.permission(UPDATE);
|
|
||||||
})
|
|
||||||
.createSubRole(AGENT)
|
|
||||||
.createSubRole(TENANT, (with) -> {
|
|
||||||
with.outgoingSubRole("debitorRel", TENANT);
|
|
||||||
with.permission(SELECT);
|
|
||||||
})
|
|
||||||
|
|
||||||
.limitDiagramTo("project", "debitorRel", "rbac.global");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void main(String[] args) throws IOException {
|
|
||||||
rbac().generateWithBaseFileName("6-hs-booking/620-booking-project/6203-hs-booking-project-rbac");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.booking.generated.api.v1.api.HsBookingProjec
|
|||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectInsertResource;
|
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.HsBookingProjectPatchResource;
|
||||||
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsBookingProjectResource;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -25,7 +25,7 @@ public class HsBookingProjectController implements HsBookingProjectsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsBookingProjectRbacRepository bookingProjectRepo;
|
private HsBookingProjectRbacRepository bookingProjectRepo;
|
||||||
|
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.fetchedBySql
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_booking_project_rv")
|
@Table(schema = "hs_booking", name = "project_rv")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@ -43,7 +43,7 @@ public class HsBookingProjectRbacEntity extends HsBookingProject {
|
|||||||
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
return rbacViewFor("project", HsBookingProjectRbacEntity.class)
|
||||||
.withIdentityView(SQL.query("""
|
.withIdentityView(SQL.query("""
|
||||||
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
SELECT bookingProject.uuid as uuid, debitorIV.idName || '-' || base.cleanIdentifier(bookingProject.caption) as idName
|
||||||
FROM hs_booking_project bookingProject
|
FROM hs_booking.project bookingProject
|
||||||
JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
JOIN hs_office.debitor_iv debitorIV ON debitorIV.uuid = bookingProject.debitorUuid
|
||||||
"""))
|
"""))
|
||||||
.withRestrictedViewOrderBy(SQL.expression("caption"))
|
.withRestrictedViewOrderBy(SQL.expression("caption"))
|
||||||
|
@ -10,7 +10,7 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_booking_project")
|
@Table(schema = "hs_booking", name = "project")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -14,7 +14,7 @@ import net.hostsharing.hsadminng.hs.booking.project.HsBookingProject;
|
|||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
import net.hostsharing.hsadminng.hs.validation.PropertiesProvider;
|
||||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.Type;
|
import org.hibernate.annotations.Type;
|
||||||
@ -89,10 +89,9 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
|||||||
@JoinColumn(name = "alarmcontactuuid")
|
@JoinColumn(name = "alarmcontactuuid")
|
||||||
private HsOfficeContactRealEntity alarmContact;
|
private HsOfficeContactRealEntity alarmContact;
|
||||||
|
|
||||||
@Builder.Default
|
|
||||||
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
|
@OneToMany(cascade = CascadeType.REFRESH, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
|
@JoinColumn(name = "parentassetuuid", referencedColumnName = "uuid")
|
||||||
private List<HsHostingAssetRealEntity> subHostingAssets = new ArrayList<>();
|
private List<HsHostingAssetRealEntity> subHostingAssets;
|
||||||
|
|
||||||
@Column(name = "identifier")
|
@Column(name = "identifier")
|
||||||
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
|
private String identifier; // e.g. vm1234, xyz00, example.org, xyz00_abc
|
||||||
@ -125,6 +124,13 @@ public abstract class HsHostingAsset implements Stringifyable, BaseEntity<HsHost
|
|||||||
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
|
PatchableMapWrapper.of(configWrapper, (newWrapper) -> {configWrapper = newWrapper;}, config).assign(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<HsHostingAssetRealEntity> getSubHostingAssets() {
|
||||||
|
if (subHostingAssets == null) {
|
||||||
|
subHostingAssets = new ArrayList<>();
|
||||||
|
}
|
||||||
|
return subHostingAssets;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PatchableMapWrapper<Object> directProps() {
|
public PatchableMapWrapper<Object> directProps() {
|
||||||
return getConfig();
|
return getConfig();
|
||||||
|
@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAsse
|
|||||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetResource;
|
||||||
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
import net.hostsharing.hsadminng.hs.hosting.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
import net.hostsharing.hsadminng.mapper.KeyValueMap;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -35,7 +35,7 @@ public class HsHostingAssetController implements HsHostingAssetsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsHostingAssetRbacRepository rbacAssetRepo;
|
private HsHostingAssetRbacRepository rbacAssetRepo;
|
||||||
|
@ -33,7 +33,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.directlyFetc
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_hosting_asset_rv")
|
@Table(schema = "hs_hosting", name = "asset_rv")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -25,15 +25,15 @@ public interface HsHostingAssetRbacRepository extends HsHostingAssetRepository<H
|
|||||||
ha.parentassetuuid,
|
ha.parentassetuuid,
|
||||||
ha.type,
|
ha.type,
|
||||||
ha.version
|
ha.version
|
||||||
from hs_hosting_asset_rv ha
|
from hs_hosting.asset_rv ha
|
||||||
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid
|
left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
|
||||||
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid
|
left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
|
||||||
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
||||||
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
||||||
and (:type is null or :type=cast(ha.type as text))
|
and (:type is null or :type=cast(ha.type as text))
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
// The JPQL query did not generate "left join" but just "join".
|
// 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.
|
// 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<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
List<HsHostingAssetRbacEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||||
default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
default List<HsHostingAssetRbacEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||||
|
@ -9,7 +9,7 @@ import jakarta.persistence.Entity;
|
|||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "hs_hosting_asset")
|
@Table(schema = "hs_hosting", name = "asset")
|
||||||
@SuperBuilder(builderMethodName = "genericBuilder", toBuilder = true)
|
@SuperBuilder(builderMethodName = "genericBuilder", toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.hosting.asset;
|
|||||||
import org.springframework.data.jpa.repository.Query;
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.Repository;
|
import org.springframework.data.repository.Repository;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@ -13,6 +14,18 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
|
|||||||
|
|
||||||
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
List<HsHostingAssetRealEntity> findByIdentifier(String assetIdentifier);
|
||||||
|
|
||||||
|
default List<HsHostingAssetRealEntity> findByTypeAndIdentifier(@NotNull HsHostingAssetType type, @NotNull String identifier) {
|
||||||
|
return findByTypeAndIdentifierImpl(type.name(), identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query("""
|
||||||
|
select ha
|
||||||
|
from HsHostingAssetRealEntity ha
|
||||||
|
where cast(ha.type as String) = :type
|
||||||
|
and ha.identifier = :identifier
|
||||||
|
""")
|
||||||
|
List<HsHostingAssetRealEntity> findByTypeAndIdentifierImpl(@NotNull String type, @NotNull String identifier);
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
select ha.uuid,
|
select ha.uuid,
|
||||||
ha.alarmcontactuuid,
|
ha.alarmcontactuuid,
|
||||||
@ -24,15 +37,15 @@ public interface HsHostingAssetRealRepository extends HsHostingAssetRepository<H
|
|||||||
ha.parentassetuuid,
|
ha.parentassetuuid,
|
||||||
ha.type,
|
ha.type,
|
||||||
ha.version
|
ha.version
|
||||||
from hs_hosting_asset_rv ha
|
from hs_hosting.asset_rv ha
|
||||||
left join hs_booking_item bi on bi.uuid = ha.bookingitemuuid
|
left join hs_booking.item bi on bi.uuid = ha.bookingitemuuid
|
||||||
left join hs_hosting_asset pha on pha.uuid = ha.parentassetuuid
|
left join hs_hosting.asset pha on pha.uuid = ha.parentassetuuid
|
||||||
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
where (:projectUuid is null or bi.projectuuid=:projectUuid)
|
||||||
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
and (:parentAssetUuid is null or pha.uuid=:parentAssetUuid)
|
||||||
and (:type is null or :type=cast(ha.type as text))
|
and (:type is null or :type=cast(ha.type as text))
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
// The JPQL query did not generate "left join" but just "join".
|
// 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.
|
// 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<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
List<HsHostingAssetRealEntity> findAllByCriteriaImpl(UUID projectUuid, UUID parentAssetUuid, String type);
|
||||||
default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
default List<HsHostingAssetRealEntity> findAllByCriteria(final UUID projectUuid, final UUID parentAssetUuid, final HsHostingAssetType type) {
|
||||||
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
return findAllByCriteriaImpl(projectUuid, parentAssetUuid, HsHostingAssetType.asString(type));
|
||||||
|
@ -0,0 +1,159 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetSubInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
|
import net.hostsharing.hsadminng.mapper.ToStringConverter;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import java.net.IDN;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_DNS_SETUP;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_HTTP_SETUP;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_MBOX_SETUP;
|
||||||
|
import static net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetType.DOMAIN_SMTP_SETUP;
|
||||||
|
|
||||||
|
public class DomainSetupHostingAssetFactory extends HostingAssetFactory {
|
||||||
|
|
||||||
|
public DomainSetupHostingAssetFactory(
|
||||||
|
final EntityManagerWrapper emw,
|
||||||
|
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||||
|
final HsHostingAssetAutoInsertResource asset,
|
||||||
|
final StandardMapper standardMapper) {
|
||||||
|
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HsHostingAsset create() {
|
||||||
|
final var domainSetupAsset = createDomainSetupAsset(getDomainName());
|
||||||
|
final var subHostingAssets = domainSetupAsset.getSubHostingAssets();
|
||||||
|
|
||||||
|
// TODO.legacy: as long as we need to be compatible, we always do all technical domain-setups
|
||||||
|
|
||||||
|
final var domainHttpSetupAssetResource = findSubHostingAssetResource(HsHostingAssetTypeResource.DOMAIN_HTTP_SETUP);
|
||||||
|
final var assignedToUnixUserAssetEntity = domainHttpSetupAssetResource
|
||||||
|
.map(HsHostingAssetSubInsertResource::getAssignedToAssetUuid)
|
||||||
|
.map(uuid -> emw.find(HsHostingAssetRealEntity.class, uuid))
|
||||||
|
.orElseThrow(() -> new ValidationException("DOMAIN_HTTP_SETUP subAsset with assignedToAssetUuid required in compatibility mode"));
|
||||||
|
|
||||||
|
subHostingAssets.add(
|
||||||
|
createDomainSubSetupAssetEntity(
|
||||||
|
domainSetupAsset,
|
||||||
|
DOMAIN_HTTP_SETUP,
|
||||||
|
builder -> builder
|
||||||
|
.assignedToAsset(assignedToUnixUserAssetEntity)
|
||||||
|
.identifier(getDomainName() + "|HTTP")
|
||||||
|
.caption("HTTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do not add to subHostingAssets in compatibility mode, in this case, DNS setup works via file system.
|
||||||
|
// The entity is created just for validation purposes.
|
||||||
|
createDomainSubSetupAssetEntity(
|
||||||
|
domainSetupAsset,
|
||||||
|
DOMAIN_DNS_SETUP,
|
||||||
|
builder -> builder
|
||||||
|
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||||
|
.identifier(getDomainName() + "|DNS")
|
||||||
|
.caption("DNS-Setup für " + IDN.toUnicode(getDomainName())));
|
||||||
|
|
||||||
|
subHostingAssets.add(
|
||||||
|
createDomainSubSetupAssetEntity(
|
||||||
|
domainSetupAsset,
|
||||||
|
DOMAIN_MBOX_SETUP,
|
||||||
|
builder -> builder
|
||||||
|
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||||
|
.identifier(getDomainName() + "|MBOX")
|
||||||
|
.caption("MBOX-Setup für " + IDN.toUnicode(getDomainName())))
|
||||||
|
);
|
||||||
|
|
||||||
|
subHostingAssets.add(
|
||||||
|
createDomainSubSetupAssetEntity(
|
||||||
|
domainSetupAsset,
|
||||||
|
DOMAIN_SMTP_SETUP,
|
||||||
|
builder -> builder
|
||||||
|
.assignedToAsset(assignedToUnixUserAssetEntity.getParentAsset())
|
||||||
|
.identifier(getDomainName() + "|SMTP")
|
||||||
|
.caption("SMTP-Setup für " + IDN.toUnicode(getDomainName())))
|
||||||
|
);
|
||||||
|
|
||||||
|
return domainSetupAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsHostingAssetRealEntity createDomainSetupAsset(final String domainName) {
|
||||||
|
return HsHostingAssetRealEntity.builder()
|
||||||
|
.bookingItem(fromBookingItem)
|
||||||
|
.type(HsHostingAssetType.DOMAIN_SETUP)
|
||||||
|
.identifier(domainName)
|
||||||
|
.caption(asset.getCaption() != null ? asset.getCaption() : domainName)
|
||||||
|
.alarmContact(ref(HsOfficeContactRealEntity.class, asset.getAlarmContactUuid()))
|
||||||
|
// the sub-hosting-assets get added later
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsHostingAssetRealEntity createDomainSubSetupAssetEntity(
|
||||||
|
final HsHostingAssetRealEntity domainSetupAsset,
|
||||||
|
final HsHostingAssetType subAssetType,
|
||||||
|
final Function<HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>, HsHostingAssetRealEntity.HsHostingAssetRealEntityBuilder<?, ?>> builderTransformer) {
|
||||||
|
final var resourceType = HsHostingAssetTypeResource.valueOf(subAssetType.name());
|
||||||
|
|
||||||
|
final var subAssetResourceOptional = findSubHostingAssetResource(resourceType);
|
||||||
|
|
||||||
|
subAssetResourceOptional.ifPresentOrElse(
|
||||||
|
this::verifyNotOverspecified,
|
||||||
|
() -> { throw new ValidationException("sub-asset of type " + resourceType.name() + " required in legacy mode, but missing"); }
|
||||||
|
);
|
||||||
|
|
||||||
|
return builderTransformer.apply(
|
||||||
|
HsHostingAssetRealEntity.builder()
|
||||||
|
.type(subAssetType)
|
||||||
|
.parentAsset(domainSetupAsset))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<HsHostingAssetSubInsertResource> findSubHostingAssetResource(final HsHostingAssetTypeResource resourceType) {
|
||||||
|
return getSubHostingAssetResources().stream()
|
||||||
|
.filter(ha -> ha.getType() == resourceType)
|
||||||
|
.reduce(Reducer::toSingleElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO.legacy: while we need to stay compatible, only default values can be used, thus only the type can be specified
|
||||||
|
private void verifyNotOverspecified(final HsHostingAssetSubInsertResource givenSubAssetResource) {
|
||||||
|
final var convert = new ToStringConverter().ignoring("assignedToAssetUuid");
|
||||||
|
final var expectedSubAssetResource = new HsHostingAssetSubInsertResource();
|
||||||
|
expectedSubAssetResource.setType(givenSubAssetResource.getType());
|
||||||
|
if ( !convert.from(givenSubAssetResource).equals(convert.from(expectedSubAssetResource)) ) {
|
||||||
|
throw new ValidationException("sub asset " + givenSubAssetResource.getType() + " is over-specified, in compatibility mode, only default values allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getDomainName() {
|
||||||
|
return asset.getIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HsHostingAssetSubInsertResource> getSubHostingAssetResources() {
|
||||||
|
return asset.getSubHostingAssets();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||||
|
super.persist(newHostingAsset);
|
||||||
|
newHostingAsset.getSubHostingAssets().forEach(super::persist);
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> T ref(final Class<T> entityClass, final UUID uuid) {
|
||||||
|
return uuid != null ? emw.getReference(entityClass, uuid) : null;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.validators.HostingAssetEntitySaveProcessor;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
abstract class HostingAssetFactory {
|
||||||
|
|
||||||
|
final EntityManagerWrapper emw;
|
||||||
|
final HsBookingItemRealEntity fromBookingItem;
|
||||||
|
final HsHostingAssetAutoInsertResource asset;
|
||||||
|
final StandardMapper standardMapper;
|
||||||
|
|
||||||
|
protected abstract HsHostingAsset create();
|
||||||
|
|
||||||
|
public String createAndPersist() {
|
||||||
|
try {
|
||||||
|
final HsHostingAsset newHostingAsset = create();
|
||||||
|
persist(newHostingAsset);
|
||||||
|
return null;
|
||||||
|
} catch (final ValidationException exc) {
|
||||||
|
return exc.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void persist(final HsHostingAsset newHostingAsset) {
|
||||||
|
new HostingAssetEntitySaveProcessor(emw, newHostingAsset)
|
||||||
|
.preprocessEntity()
|
||||||
|
.validateEntity()
|
||||||
|
.prepareForSave()
|
||||||
|
.save()
|
||||||
|
.validateContext();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.BookingItemCreatedAppEvent;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.ApplicationListener;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class HsBookingItemCreatedListener implements ApplicationListener<BookingItemCreatedAppEvent> {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EntityManagerWrapper emw;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ObjectMapper jsonMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StandardMapper standardMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
public void onApplicationEvent(@NotNull BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||||
|
if (containsAssetJson(bookingItemCreatedAppEvent)) {
|
||||||
|
createRelatedHostingAsset(bookingItemCreatedAppEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean containsAssetJson(final BookingItemCreatedAppEvent bookingItemCreatedAppEvent) {
|
||||||
|
return bookingItemCreatedAppEvent.getEntity().getAssetJson() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createRelatedHostingAsset(final BookingItemCreatedAppEvent event) throws JsonProcessingException {
|
||||||
|
final var newBookingItemRealEntity = event.getEntity().getBookingItem();
|
||||||
|
final var asset = jsonMapper.readValue(event.getEntity().getAssetJson(), HsHostingAssetAutoInsertResource.class);
|
||||||
|
final var factory = switch (newBookingItemRealEntity.getType()) {
|
||||||
|
case PRIVATE_CLOUD, CLOUD_SERVER, MANAGED_SERVER ->
|
||||||
|
forNowNoAutomaticHostingAssetCreationPossible(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||||
|
case MANAGED_WEBSPACE -> new ManagedWebspaceHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||||
|
case DOMAIN_SETUP -> new DomainSetupHostingAssetFactory(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||||
|
};
|
||||||
|
if (factory != null) {
|
||||||
|
final var statusMessage = factory.createAndPersist();
|
||||||
|
// TODO.impl: once we implement retry, we need to amend this code (persist/merge/delete)
|
||||||
|
if (statusMessage != null) {
|
||||||
|
event.getEntity().setStatusMessage(statusMessage);
|
||||||
|
emw.persist(event.getEntity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HostingAssetFactory forNowNoAutomaticHostingAssetCreationPossible(
|
||||||
|
final EntityManagerWrapper emw,
|
||||||
|
final HsBookingItemRealEntity fromBookingItem,
|
||||||
|
final HsHostingAssetAutoInsertResource asset,
|
||||||
|
final StandardMapper standardMapper
|
||||||
|
) {
|
||||||
|
return new HostingAssetFactory(emw, fromBookingItem, asset, standardMapper) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HsHostingAsset create() {
|
||||||
|
// TODO.impl: we should validate the asset JSON, but some violations are un-avoidable at that stage
|
||||||
|
throw new ValidationException("waiting for manual setup of hosting asset for booking item of type " + fromBookingItem.getType());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.hosting.asset.factories;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetAutoInsertResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.generated.api.v1.model.HsHostingAssetTypeResource;
|
||||||
|
import net.hostsharing.hsadminng.hs.booking.item.HsBookingItemRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAsset;
|
||||||
|
import net.hostsharing.hsadminng.hs.hosting.asset.HsHostingAssetRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
|
||||||
|
public class ManagedWebspaceHostingAssetFactory extends HostingAssetFactory {
|
||||||
|
|
||||||
|
public ManagedWebspaceHostingAssetFactory(
|
||||||
|
final EntityManagerWrapper emw,
|
||||||
|
final HsBookingItemRealEntity newBookingItemRealEntity,
|
||||||
|
final HsHostingAssetAutoInsertResource asset,
|
||||||
|
final StandardMapper standardMapper) {
|
||||||
|
super(emw, newBookingItemRealEntity, asset, standardMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HsHostingAsset create() {
|
||||||
|
if (asset.getType() != HsHostingAssetTypeResource.MANAGED_WEBSPACE) {
|
||||||
|
throw new ValidationException("requires MANAGED_WEBSPACE hosting asset, but got " +
|
||||||
|
Optional.of(asset)
|
||||||
|
.map(HsHostingAssetAutoInsertResource::getType)
|
||||||
|
.map(Enum::name)
|
||||||
|
.orElse(null));
|
||||||
|
}
|
||||||
|
final var managedWebspaceHostingAsset = standardMapper.map(asset, HsHostingAssetRealEntity.class);
|
||||||
|
managedWebspaceHostingAsset.setBookingItem(fromBookingItem);
|
||||||
|
emw.createQuery(
|
||||||
|
"SELECT asset FROM HsHostingAssetRealEntity asset WHERE asset.bookingItem.uuid=:bookingItemUuid",
|
||||||
|
HsHostingAssetRealEntity.class)
|
||||||
|
.setParameter("bookingItemUuid", fromBookingItem.getParentItem().getUuid())
|
||||||
|
.getResultStream().findFirst()
|
||||||
|
.ifPresent(managedWebspaceHostingAsset::setParentAsset);
|
||||||
|
|
||||||
|
return managedWebspaceHostingAsset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void persist(final HsHostingAsset newManagedWebspaceHostingAsset) {
|
||||||
|
super.persist(newManagedWebspaceHostingAsset);
|
||||||
|
}
|
||||||
|
}
|
@ -42,7 +42,7 @@ public class HostingAssetEntitySaveProcessor {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO.impl: remove once the migration of legacy data is done
|
// TODO.legacy: remove once the migration of legacy data is done
|
||||||
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
/// validates the entity itself including its properties, but ignoring some error messages for import of legacy data
|
||||||
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
public HostingAssetEntitySaveProcessor validateEntityIgnoring(final String... ignoreRegExp) {
|
||||||
step("validateEntity", "prepareForSave");
|
step("validateEntity", "prepareForSave");
|
||||||
|
@ -15,7 +15,7 @@ import static net.hostsharing.hsadminng.hs.validation.BooleanProperty.booleanPro
|
|||||||
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
import static net.hostsharing.hsadminng.hs.validation.IntegerProperty.integerProperty;
|
||||||
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
import static net.hostsharing.hsadminng.hs.validation.StringProperty.stringProperty;
|
||||||
|
|
||||||
// TODO.impl: make package private once we've migrated the legacy data
|
// TODO.legacy: make package private once we've migrated the legacy data
|
||||||
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityValidator {
|
||||||
|
|
||||||
// according to RFC 1035 (section 5) and RFC 1034
|
// according to RFC 1035 (section 5) and RFC 1034
|
||||||
@ -33,7 +33,7 @@ public class HsDomainDnsSetupHostingAssetValidator extends HostingAssetEntityVal
|
|||||||
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
RR_REGEX_NAME + RR_REGEX_IN + RR_REGEX_TTL + RR_RECORD_TYPE + RR_RECORD_DATA + RR_COMMENT;
|
||||||
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
public static final String IDENTIFIER_SUFFIX = "|DNS";
|
||||||
|
|
||||||
private static List<String> zoneFileErrors = null; // TODO.impl: remove once legacy data is migrated
|
private static List<String> zoneFileErrors = null; // TODO.legacy: remove once legacy data is migrated
|
||||||
|
|
||||||
HsDomainDnsSetupHostingAssetValidator() {
|
HsDomainDnsSetupHostingAssetValidator() {
|
||||||
super(
|
super(
|
||||||
|
@ -53,7 +53,7 @@ class HsUnixUserHostingAssetValidator extends HostingAssetEntityValidator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static Integer computeUserId(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
private static Integer computeUserId(final EntityManager em, final PropertiesProvider propertiesProvider) {
|
||||||
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting_asset_unixuser_system_id_seq')", Integer.class)
|
final Object result = em.createNativeQuery("SELECT nextval('hs_hosting.asset_unixuser_system_id_seq')", Integer.class)
|
||||||
.getSingleResult();
|
.getSingleResult();
|
||||||
return (Integer) result;
|
return (Integer) result;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import net.hostsharing.hsadminng.context.Context;
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeBankAccountsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeBankAccountResource;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.iban4j.BicUtil;
|
import org.iban4j.BicUtil;
|
||||||
import org.iban4j.IbanUtil;
|
import org.iban4j.IbanUtil;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -24,7 +24,7 @@ public class HsOfficeBankAccountController implements HsOfficeBankAccountsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeBankAccountRepository bankAccountRepo;
|
private HsOfficeBankAccountRepository bankAccountRepo;
|
||||||
|
@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
@ -11,7 +11,7 @@ import lombok.experimental.FieldNameConstants;
|
|||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
import net.hostsharing.hsadminng.mapper.PatchableMapWrapper;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
import org.hibernate.annotations.GenericGenerator;
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.contact;
|
package net.hostsharing.hsadminng.hs.office.contact;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeContactsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeContactInsertResource;
|
||||||
@ -26,7 +26,7 @@ public class HsOfficeContactController implements HsOfficeContactsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeContactRbacRepository contactRepo;
|
private HsOfficeContactRbacRepository contactRepo;
|
||||||
|
@ -4,7 +4,7 @@ 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.api.HsOfficeCoopAssetsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||||
@ -29,7 +29,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
|
private HsOfficeCoopAssetsTransactionRepository coopAssetsTransactionRepo;
|
||||||
|
@ -8,7 +8,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
|||||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(schema = "hs_office", name = "coopassetstransaction_rv")
|
@Table(schema = "hs_office", name = "coopassettx_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
|
@ -6,7 +6,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeCoopShar
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionInsertResource;
|
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.hs.office.generated.api.v1.model.HsOfficeCoopSharesTransactionResource;
|
||||||
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
import net.hostsharing.hsadminng.errors.MultiValidationException;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.format.annotation.DateTimeFormat;
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
import org.springframework.format.annotation.DateTimeFormat.ISO;
|
||||||
@ -31,7 +31,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
|
private HsOfficeCoopSharesTransactionRepository coopSharesTransactionRepo;
|
||||||
|
@ -8,7 +8,7 @@ import lombok.Setter;
|
|||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
|||||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(schema = "hs_office", name = "coopsharestransaction_rv")
|
@Table(schema = "hs_office", name = "coopsharetx_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
|
@ -7,8 +7,8 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebito
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.EntityExistsValidator;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hibernate.Hibernate;
|
import org.hibernate.Hibernate;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -23,7 +23,6 @@ import jakarta.validation.ValidationException;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
|
||||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ -34,7 +33,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeDebitorRepository debitorRepo;
|
private HsOfficeDebitorRepository debitorRepo;
|
||||||
@ -42,6 +41,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeRelationRealRepository relrealRepo;
|
private HsOfficeRelationRealRepository relrealRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EntityExistsValidator entityValidator;
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
private EntityManager em;
|
private EntityManager em;
|
||||||
|
|
||||||
@ -84,10 +86,10 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||||
if ( body.getDebitorRel() != null ) {
|
if ( body.getDebitorRel() != null ) {
|
||||||
body.getDebitorRel().setType(DEBITOR.name());
|
body.getDebitorRel().setType(DEBITOR.name());
|
||||||
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
final var debitorRel = mapper.map("debitorRel.", body.getDebitorRel(), HsOfficeRelationRealEntity.class);
|
||||||
validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
entityValidator.validateEntityExists("debitorRel.anchorUuid", debitorRel.getAnchor());
|
||||||
validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
entityValidator.validateEntityExists("debitorRel.holderUuid", debitorRel.getHolder());
|
||||||
validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
entityValidator.validateEntityExists("debitorRel.contactUuid", debitorRel.getContact());
|
||||||
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
entityToSave.setDebitorRel(relrealRepo.save(debitorRel));
|
||||||
} else {
|
} else {
|
||||||
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
final var debitorRelOptional = relrealRepo.findByUuid(body.getDebitorRelUuid());
|
||||||
@ -160,15 +162,4 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
|||||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO.impl: extract this to some generally usable class?
|
|
||||||
private <T extends BaseEntity<T>> T validateEntityExists(final String property, final T entitySkeleton) {
|
|
||||||
final var foundEntity = em.find(entitySkeleton.getClass(), entitySkeleton.getUuid());
|
|
||||||
if ( foundEntity == null) {
|
|
||||||
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
|
|
||||||
}
|
|
||||||
|
|
||||||
//noinspection unchecked
|
|
||||||
return (T) foundEntity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
|||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeMembersh
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipResource;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -24,7 +24,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeMembershipRepository membershipRepo;
|
private HsOfficeMembershipRepository membershipRepo;
|
||||||
|
@ -9,7 +9,7 @@ import lombok.NoArgsConstructor;
|
|||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
|
@ -2,18 +2,18 @@ package net.hostsharing.hsadminng.hs.office.membership;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||||
|
|
||||||
private final Mapper mapper;
|
private final StandardMapper mapper;
|
||||||
private final HsOfficeMembershipEntity entity;
|
private final HsOfficeMembershipEntity entity;
|
||||||
|
|
||||||
public HsOfficeMembershipEntityPatcher(
|
public HsOfficeMembershipEntityPatcher(
|
||||||
final Mapper mapper,
|
final StandardMapper mapper,
|
||||||
final HsOfficeMembershipEntity entity) {
|
final HsOfficeMembershipEntity entity) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
|
@ -12,8 +12,8 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
|||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@ -36,7 +36,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePartnerRepository partnerRepo;
|
private HsOfficePartnerRepository partnerRepo;
|
||||||
|
@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
|||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
|
@ -10,7 +10,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContact;
|
|||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelation;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.person;
|
package net.hostsharing.hsadminng.hs.office.person;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePersonsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePersonInsertResource;
|
||||||
@ -23,7 +23,7 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePersonRepository personRepo;
|
private HsOfficePersonRepository personRepo;
|
||||||
|
@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
|
|||||||
import lombok.*;
|
import lombok.*;
|
||||||
import lombok.experimental.FieldNameConstants;
|
import lombok.experimental.FieldNameConstants;
|
||||||
import net.hostsharing.hsadminng.errors.DisplayAs;
|
import net.hostsharing.hsadminng.errors.DisplayAs;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
@ -21,6 +21,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.rbacViewFor;
|
||||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||||
|
|
||||||
|
// TODO.refa: split HsOfficePersonEntity into Real+Rbac-Entity
|
||||||
@Entity
|
@Entity
|
||||||
@Table(schema = "hs_office", name = "person_rv")
|
@Table(schema = "hs_office", name = "person_rv")
|
||||||
@Getter
|
@Getter
|
||||||
|
@ -5,7 +5,7 @@ import lombok.experimental.FieldNameConstants;
|
|||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealRepository
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationsApi;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.*;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -28,7 +28,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeRelationRbacRepository relationRbacRepo;
|
private HsOfficeRelationRbacRepository relationRbacRepo;
|
||||||
@ -37,7 +37,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
|||||||
private HsOfficePersonRepository holderRepo;
|
private HsOfficePersonRepository holderRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeContactRealRepository contactrealRepo;
|
private HsOfficeContactRealRepository realContactRepo;
|
||||||
|
|
||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
private EntityManager em;
|
private EntityManager em;
|
||||||
@ -48,11 +48,16 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
|||||||
final String currentSubject,
|
final String currentSubject,
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID personUuid,
|
final UUID personUuid,
|
||||||
final HsOfficeRelationTypeResource relationType) {
|
final HsOfficeRelationTypeResource relationType,
|
||||||
|
final String personData,
|
||||||
|
final String contactData) {
|
||||||
context.define(currentSubject, assumedRoles);
|
context.define(currentSubject, assumedRoles);
|
||||||
|
|
||||||
final var entities = relationRbacRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid,
|
final List<HsOfficeRelationRbacEntity> entities =
|
||||||
mapper.map(relationType, HsOfficeRelationType.class));
|
relationRbacRepo.findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
|
||||||
|
personUuid,
|
||||||
|
relationType == null ? null : HsOfficeRelationType.valueOf(relationType.name()),
|
||||||
|
personData, contactData);
|
||||||
|
|
||||||
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
|
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
|
||||||
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
@ -77,7 +82,7 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
|||||||
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
|
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
|
||||||
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
|
() -> new NoSuchElementException("cannot find Person by holderUuid: " + body.getHolderUuid())
|
||||||
));
|
));
|
||||||
entityToSave.setContact(contactrealRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
entityToSave.setContact(realContactRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
||||||
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
|
() -> new NoSuchElementException("cannot find Contact by contactUuid: " + body.getContactUuid())
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -144,7 +149,6 @@ public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
|||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
final BiConsumer<HsOfficeRelationRbacEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
final BiConsumer<HsOfficeRelationRbacEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
|
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
|
||||||
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
|
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
|
||||||
|
@ -12,26 +12,62 @@ public interface HsOfficeRelationRbacRepository extends Repository<HsOfficeRelat
|
|||||||
|
|
||||||
Optional<HsOfficeRelationRbacEntity> findByUuid(UUID id);
|
Optional<HsOfficeRelationRbacEntity> findByUuid(UUID id);
|
||||||
|
|
||||||
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
|
|
||||||
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT p.* FROM hs_office.relation_rv AS p
|
SELECT p.* FROM hs_office.relation_rv AS p
|
||||||
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
|
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
|
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds relations by a conjunction of optional criteria, including anchorPerson, holderPerson and contact data.
|
||||||
|
* *
|
||||||
|
* @param personUuid the optional UUID of the anchorPerson or holderPerson
|
||||||
|
* @param relationType the type of the relation
|
||||||
|
* @param personData a string to match the persons tradeName, familyName or givenName (use '%' for wildcard), case ignored
|
||||||
|
* @param contactData a string to match the contacts caption, postalAddress, emailAddresses or phoneNumbers (use '%' for wildcard), case ignored
|
||||||
|
* @return a list of (accessible) relations which match all given criteria
|
||||||
|
*/
|
||||||
|
default List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactData(
|
||||||
|
final UUID personUuid,
|
||||||
|
final HsOfficeRelationType relationType,
|
||||||
|
final String personData,
|
||||||
|
final String contactData) {
|
||||||
|
return findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
|
||||||
|
personUuid, toStringOrNull(relationType), toSqlLikeOperand(personData), toSqlLikeOperand(contactData));
|
||||||
|
}
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT p.* FROM hs_office.relation_rv AS p
|
SELECT rel FROM HsOfficeRelationRbacEntity AS rel
|
||||||
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType))
|
WHERE (:relationType IS NULL OR CAST(rel.type AS String) = :relationType)
|
||||||
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
|
AND ( :personUuid IS NULL
|
||||||
""", nativeQuery = true)
|
OR rel.anchor.uuid = :personUuid OR rel.holder.uuid = :personUuid )
|
||||||
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
|
AND ( :personData IS NULL
|
||||||
|
OR lower(rel.anchor.tradeName) LIKE :personData OR lower(rel.holder.tradeName) LIKE :personData
|
||||||
|
OR lower(rel.anchor.familyName) LIKE :personData OR lower(rel.holder.familyName) LIKE :personData
|
||||||
|
OR lower(rel.anchor.givenName) LIKE :personData OR lower(rel.holder.givenName) LIKE :personData )
|
||||||
|
AND ( :contactData IS NULL
|
||||||
|
OR lower(rel.contact.caption) LIKE :contactData
|
||||||
|
OR lower(rel.contact.postalAddress) LIKE :contactData
|
||||||
|
OR lower(CAST(rel.contact.emailAddresses AS String)) LIKE :contactData
|
||||||
|
OR lower(CAST(rel.contact.phoneNumbers AS String)) LIKE :contactData )
|
||||||
|
""")
|
||||||
|
List<HsOfficeRelationRbacEntity> findRelationRelatedToPersonUuidRelationTypePersonAndContactDataImpl(
|
||||||
|
final UUID personUuid,
|
||||||
|
final String relationType,
|
||||||
|
final String personData,
|
||||||
|
final String contactData);
|
||||||
|
|
||||||
HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity);
|
HsOfficeRelationRbacEntity save(final HsOfficeRelationRbacEntity entity);
|
||||||
|
|
||||||
long count();
|
long count();
|
||||||
|
|
||||||
int deleteByUuid(UUID uuid);
|
int deleteByUuid(UUID uuid);
|
||||||
|
|
||||||
|
private static String toSqlLikeOperand(final String text) {
|
||||||
|
return text == null ? null : ("%" + text.toLowerCase() + "%");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toStringOrNull(final HsOfficeRelationType relationType) {
|
||||||
|
return relationType == null ? null : relationType.name();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
|
|||||||
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
|
Optional<HsOfficeRelationRealEntity> findByUuid(UUID id);
|
||||||
|
|
||||||
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
|
default List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
|
||||||
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
|
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType == null ? null : relationType.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
@ -24,7 +24,7 @@ public interface HsOfficeRelationRealRepository extends Repository<HsOfficeRelat
|
|||||||
|
|
||||||
@Query(value = """
|
@Query(value = """
|
||||||
SELECT p.* FROM hs_office.relation AS p
|
SELECT p.* FROM hs_office.relation AS p
|
||||||
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType))
|
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS hs_office.RelationType))
|
||||||
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
|
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
|
||||||
""", nativeQuery = true)
|
""", nativeQuery = true)
|
||||||
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
|
List<HsOfficeRelationRealEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
|
||||||
|
@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeSepaMand
|
|||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateInsertResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandatePatchResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
|
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeSepaMandateResource;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@ -28,7 +28,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficeSepaMandateRepository sepaMandateRepo;
|
private HsOfficeSepaMandateRepository sepaMandateRepo;
|
||||||
|
@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.errors.DisplayAs;
|
|||||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||||
|
@ -130,7 +130,7 @@ public abstract class HsEntityValidator<E extends PropertiesProvider> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) {
|
public Map<String, Object> revampProperties(final EntityManager em, final E entity, final Map<String, Object> config) {
|
||||||
final var copy = new HashMap<>(config);
|
final var copy = config != null ? new HashMap<>(config) : new HashMap();
|
||||||
stream(propertyValidators).forEach(p -> {
|
stream(propertyValidators).forEach(p -> {
|
||||||
if (p.isWriteOnly()) {
|
if (p.isWriteOnly()) {
|
||||||
copy.remove(p.propertyName);
|
copy.remove(p.propertyName);
|
||||||
|
@ -56,6 +56,10 @@ public class IntegerProperty<P extends IntegerProperty<P>> extends ValidatablePr
|
|||||||
return unit;
|
return unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer min() {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer max() {
|
public Integer max() {
|
||||||
return max;
|
return max;
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,7 @@ public class PasswordProperty extends StringProperty<PasswordProperty> {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
protected void validate(final List<String> result, final String propValue, final PropertiesProvider propProvider) {
|
||||||
// TODO.impl: remove after legacy data is migrated
|
// TODO.legacy: remove after legacy data is migrated
|
||||||
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
|
if (HashGenerator.using(hashedUsing).couldBeHash(propValue) && propValue.length() > this.maxLength()) {
|
||||||
// already hashed => do not validate
|
// already hashed => do not validate
|
||||||
return;
|
return;
|
||||||
|
@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.validation;
|
|||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.mapper.Array;
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -83,11 +84,15 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// predefined values, similar to fixed values in a combobox
|
/// predefined values, similar to fixed values in a combobox
|
||||||
public P provided(final String... provided) {
|
public P provided(final String firstProvidedValue, final String... moreProvidedValues) {
|
||||||
this.provided = provided;
|
this.provided = ArrayUtils.addAll(new String[]{firstProvidedValue}, moreProvidedValues);
|
||||||
return self();
|
return self();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String[] provided() {
|
||||||
|
return this.provided;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The property value is not disclosed in error messages.
|
* The property value is not disclosed in error messages.
|
||||||
*
|
*
|
||||||
@ -109,7 +114,11 @@ public class StringProperty<P extends StringProperty<P>> extends ValidatableProp
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String display(final String propValue) {
|
protected String display(final String propValue) {
|
||||||
return undisclosed ? "provided value" : ("'" + propValue + "'");
|
return undisclosed
|
||||||
|
? "provided value"
|
||||||
|
: propValue != null
|
||||||
|
? ("'" + propValue + "'")
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
11
src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java
Normal file
11
src/main/java/net/hostsharing/hsadminng/lambda/Reducer.java
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package net.hostsharing.hsadminng.lambda;
|
||||||
|
|
||||||
|
import lombok.experimental.UtilityClass;
|
||||||
|
|
||||||
|
@UtilityClass
|
||||||
|
public class Reducer {
|
||||||
|
public static <T> T toSingleElement(T ignoredLast, T ignoredNext) {
|
||||||
|
throw new AssertionError("only a single entity expected");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,28 +1,30 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import org.modelmapper.ModelMapper;
|
import org.modelmapper.ModelMapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.util.ReflectionUtils;
|
import org.springframework.util.ReflectionUtils;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
|
||||||
import jakarta.persistence.ManyToOne;
|
import jakarta.persistence.ManyToOne;
|
||||||
import jakarta.persistence.PersistenceContext;
|
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import static java.util.Arrays.stream;
|
||||||
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
import static net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A nicer API for ModelMapper.
|
* A nicer API for ModelMapper.
|
||||||
*/
|
*/
|
||||||
public class Mapper extends ModelMapper {
|
abstract class Mapper extends ModelMapper {
|
||||||
|
|
||||||
@PersistenceContext
|
EntityManagerWrapper em;
|
||||||
EntityManager em;
|
|
||||||
|
|
||||||
public Mapper() {
|
Mapper(@Autowired final EntityManagerWrapper em) {
|
||||||
|
this.em = em;
|
||||||
getConfiguration().setAmbiguityIgnored(true);
|
getConfiguration().setAmbiguityIgnored(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +47,12 @@ public class Mapper extends ModelMapper {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public <D> D map(final Object source, final Class<D> destinationType) {
|
public <D> D map(final Object source, final Class<D> destinationType) {
|
||||||
|
return map("", source, destinationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <D> D map(final String namePrefix, final Object source, final Class<D> destinationType) {
|
||||||
final var target = super.map(source, destinationType);
|
final var target = super.map(source, destinationType);
|
||||||
for (Field f : destinationType.getDeclaredFields()) {
|
for (Field f : getDeclaredFieldsIncludingSuperClasses(destinationType)) {
|
||||||
if (f.getAnnotation(ManyToOne.class) == null) {
|
if (f.getAnnotation(ManyToOne.class) == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -64,18 +70,30 @@ public class Mapper extends ModelMapper {
|
|||||||
if (subEntityUuid == null) {
|
if (subEntityUuid == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ReflectionUtils.setField(f, target, findEntityById(f.getType(), subEntityUuid));
|
ReflectionUtils.setField(f, target, fetchEntity(namePrefix + f.getName() + ".uuid", f.getType(), subEntityUuid));
|
||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object findEntityById(final Class<?> entityClass, final Object subEntityUuid) {
|
private static <D> Field[] getDeclaredFieldsIncludingSuperClasses(final Class<D> destinationType) {
|
||||||
// using getReference would be more efficent, but results in very technical error messages
|
if (destinationType == null) {
|
||||||
final var entity = em.find(entityClass, subEntityUuid);
|
return new Field[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.concat(
|
||||||
|
stream(destinationType.getDeclaredFields()),
|
||||||
|
stream(getDeclaredFieldsIncludingSuperClasses(destinationType.getSuperclass())))
|
||||||
|
.toArray(Field[]::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <E> E fetchEntity(final String propertyName, final Class<E> entityClass, final Object subEntityUuid) {
|
||||||
|
final var entity = em.getReference(entityClass, subEntityUuid);
|
||||||
if (entity != null) {
|
if (entity != null) {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
throw new ValidationException("Unable to find " + DisplayName.of(entityClass) + " by uuid: " + subEntityUuid);
|
throw new ValidationException(
|
||||||
|
"Unable to find " + DisplayName.of(entityClass) +
|
||||||
|
" by " + propertyName + ": " + subEntityUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
|
public <S, T> T map(final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
|
||||||
@ -86,4 +104,13 @@ public class Mapper extends ModelMapper {
|
|||||||
postMapper.accept(source, target);
|
postMapper.accept(source, target);
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <S, T> T map(final String namePrefix, final S source, final Class<T> targetClass, final BiConsumer<S, T> postMapper) {
|
||||||
|
if (source == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final var target = map(source, targetClass);
|
||||||
|
postMapper.accept(source, target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.mapper;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class MapperConfiguration {
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
public Mapper modelMapper() {
|
|
||||||
return new Mapper();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,17 @@
|
|||||||
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A nicer API for ModelMapper in standard mode.
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class StandardMapper extends Mapper {
|
||||||
|
|
||||||
|
public StandardMapper(@Autowired final EntityManagerWrapper em) {
|
||||||
|
super(em);
|
||||||
|
getConfiguration().setAmbiguityIgnored(true);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import static org.modelmapper.convention.MatchingStrategies.STRICT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A nicer API for ModelMapper in strict mode.
|
||||||
|
*
|
||||||
|
* <p>This makes sure that resource.whateverUuid does not accidentally get mapped to entity.uuid,
|
||||||
|
* if resource.uuid does not exist.</p>
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class StrictMapper extends Mapper {
|
||||||
|
|
||||||
|
public StrictMapper(@Autowired final EntityManagerWrapper em) {
|
||||||
|
super(em);
|
||||||
|
getConfiguration().setMatchingStrategy(STRICT);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,44 @@
|
|||||||
|
package net.hostsharing.hsadminng.mapper;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static java.util.stream.Collectors.joining;
|
||||||
|
|
||||||
|
public class ToStringConverter {
|
||||||
|
|
||||||
|
final public Set<String> ignoredFields = new HashSet<>();
|
||||||
|
|
||||||
|
public ToStringConverter ignoring(final String fieldName) {
|
||||||
|
ignoredFields.add(fieldName);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String from(final Object obj) {
|
||||||
|
return "{ " +
|
||||||
|
Arrays.stream(obj.getClass().getDeclaredFields())
|
||||||
|
.filter(f -> !ignoredFields.contains(f.getName()))
|
||||||
|
.map(field -> {
|
||||||
|
try {
|
||||||
|
field.setAccessible(true);
|
||||||
|
return field.getName() + ": " + field.get(obj);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
// ignore inaccessible fields
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.collect(joining(", "))
|
||||||
|
+ " }";
|
||||||
|
}
|
||||||
|
|
||||||
|
public String from(final Map<?, ?> map) {
|
||||||
|
return "{ "
|
||||||
|
+ map.keySet().stream()
|
||||||
|
.filter(key -> !ignoredFields.contains(key.toString()))
|
||||||
|
.sorted()
|
||||||
|
.map(k -> Map.entry(k, map.get(k)))
|
||||||
|
.map(e -> e.getKey() + ": " + e.getValue())
|
||||||
|
.collect(joining(", "))
|
||||||
|
+ " }";
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,10 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.object;
|
package net.hostsharing.hsadminng.persistence;
|
||||||
|
|
||||||
|
|
||||||
import org.hibernate.Hibernate;
|
import org.hibernate.Hibernate;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
// TODO.impl: this class does not really belong into this package, but there is no right place yet
|
|
||||||
public interface BaseEntity<T extends BaseEntity<?>> {
|
public interface BaseEntity<T extends BaseEntity<?>> {
|
||||||
UUID getUuid();
|
UUID getUuid();
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
package net.hostsharing.hsadminng.persistence;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.errors.DisplayAs.DisplayName;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EntityExistsValidator {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private EntityManagerWrapper em;
|
||||||
|
|
||||||
|
public <T extends BaseEntity<T>> void validateEntityExists(final String property, final T entitySkeleton) {
|
||||||
|
final var foundEntity = em.find(entityClass(entitySkeleton), entitySkeleton.getUuid());
|
||||||
|
if ( foundEntity == null) {
|
||||||
|
throw new ValidationException("Unable to find " + DisplayName.of(entitySkeleton) + " by " + property + ": " + entitySkeleton.getUuid());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends BaseEntity<T>> Class<?> entityClass(final T entityOrProxy) {
|
||||||
|
final var entityClass = entityClass(entityOrProxy.getClass());
|
||||||
|
if (entityClass == null) {
|
||||||
|
throw new IllegalArgumentException("@Entity not found in superclass hierarchy of " + entityOrProxy.getClass());
|
||||||
|
}
|
||||||
|
return entityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Class<?> entityClass(final Class<?> entityOrProxyClass) {
|
||||||
|
return entityOrProxyClass.isAnnotationPresent(Entity.class)
|
||||||
|
? entityOrProxyClass
|
||||||
|
: entityOrProxyClass.getSuperclass() == null
|
||||||
|
? null
|
||||||
|
: entityClass(entityOrProxyClass.getSuperclass());
|
||||||
|
}
|
||||||
|
}
|
@ -89,7 +89,7 @@ public class InsertTriggerGenerator {
|
|||||||
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row")));
|
with("superRoleRef", toRoleDescriptor(g.getSuperRoleDef(), "row")));
|
||||||
} else {
|
} else {
|
||||||
plPgSql.writeLn("""
|
plPgSql.writeLn("""
|
||||||
-- Granting INSERT INTO hs_hosting_asset permissions to specified role of pre-existing hs_hosting_asset rows slipped,
|
-- 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.
|
-- because there cannot yet be any pre-existing rows in the same table yet.
|
||||||
""",
|
""",
|
||||||
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
|
with("rawSuperTable", g.getSuperRoleDef().getEntityAlias().getRawTableNameWithSchema()),
|
||||||
@ -100,7 +100,7 @@ public class InsertTriggerGenerator {
|
|||||||
/**
|
/**
|
||||||
Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows.
|
Grants ${rawSubTable} INSERT permission to specified role of new ${rawSuperTable} rows.
|
||||||
*/
|
*/
|
||||||
create or replace function ${rawSubTableSchemaPrefix}new_${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf()
|
create or replace function ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf()
|
||||||
returns trigger
|
returns trigger
|
||||||
language plpgsql
|
language plpgsql
|
||||||
strict as $$
|
strict as $$
|
||||||
@ -113,11 +113,11 @@ public class InsertTriggerGenerator {
|
|||||||
return NEW;
|
return NEW;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
|
-- ..._z_... is to put it at the end of after insert triggers, to make sure the roles exist
|
||||||
create trigger z_new_${rawSubTableName}_grants_after_insert_tg
|
create trigger ${rawSubTableName}_z_grants_after_insert_tg
|
||||||
after insert on ${rawSuperTableWithSchema}
|
after insert on ${rawSuperTableWithSchema}
|
||||||
for each row
|
for each row
|
||||||
execute procedure ${rawSubTableSchemaPrefix}new_${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf();
|
execute procedure ${rawSubTableSchemaPrefix}${rawSubTableShortName}_grants_insert_to_${rawSuperTableShortName}_tf();
|
||||||
""",
|
""",
|
||||||
with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
|
with("ifConditionThen", g.getSuperRoleDef().getEntityAlias().isCaseDependent()
|
||||||
// TODO.impl: .type needs to be dynamically generated
|
// TODO.impl: .type needs to be dynamically generated
|
||||||
@ -325,7 +325,7 @@ public class InsertTriggerGenerator {
|
|||||||
|
|
||||||
|
|
||||||
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
|
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
|
||||||
final var functionName = toVar(roleDef);
|
final var functionName = roleDef.descriptorFunctionName();
|
||||||
if (roleDef.getEntityAlias().isGlobal()) {
|
if (roleDef.getEntityAlias().isGlobal()) {
|
||||||
return functionName + "()";
|
return functionName + "()";
|
||||||
}
|
}
|
||||||
|
@ -19,12 +19,11 @@ public class RbacRoleDescriptorsGenerator {
|
|||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset RbacRoleDescriptorsGenerator:${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS endDelimiter:--//
|
--changeset RbacRoleDescriptorsGenerator:${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
call rbac.generateRbacRoleDescriptors('${simpleEntityVarName}', '${rawTableName}');
|
call rbac.generateRbacRoleDescriptors('${rawTableName}');
|
||||||
--//
|
--//
|
||||||
|
|
||||||
""",
|
""",
|
||||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||||
with("simpleEntityVarName", simpleEntityVarName),
|
|
||||||
with("rawTableName", rawTableName));
|
with("rawTableName", rawTableName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.rbac.generator;
|
|||||||
|
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import org.reflections.Reflections;
|
import org.reflections.Reflections;
|
||||||
import org.reflections.scanners.TypeAnnotationsScanner;
|
import org.reflections.scanners.TypeAnnotationsScanner;
|
||||||
|
|
||||||
@ -29,6 +29,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinit
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacSubjectReference.UserRole.CREATOR;
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.Part.AUTO_FETCH;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.SQL.Part.AUTO_FETCH;
|
||||||
import static org.apache.commons.collections4.SetUtils.hashSet;
|
import static org.apache.commons.collections4.SetUtils.hashSet;
|
||||||
|
import static org.apache.commons.lang3.StringUtils.capitalize;
|
||||||
import static org.apache.commons.lang3.StringUtils.uncapitalize;
|
import static org.apache.commons.lang3.StringUtils.uncapitalize;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@ -830,6 +831,10 @@ public class RbacView {
|
|||||||
public boolean isGlobal(final Role role) {
|
public boolean isGlobal(final Role role) {
|
||||||
return entityAlias.isGlobal() && this.role == role;
|
return entityAlias.isGlobal() && this.role == role;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String descriptorFunctionName() {
|
||||||
|
return entityAlias.getRawTableNameWithSchema() + "_" + capitalize(role.name());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RbacSubjectReference findUserRef(final RbacSubjectReference.UserRole userRole) {
|
public RbacSubjectReference findUserRef(final RbacSubjectReference.UserRole userRole) {
|
||||||
@ -982,14 +987,12 @@ public class RbacView {
|
|||||||
|
|
||||||
String getRawTableShortName() {
|
String getRawTableShortName() {
|
||||||
// TODO.impl: some combined function and trigger names are too long
|
// TODO.impl: some combined function and trigger names are too long
|
||||||
// maybe we should shorten the table name e.g. hs_office.coopsharestransaction -> hsof.coopsharetx
|
// maybe we should shorten the table name e.g. hs_office.coopsharetx -> hsof.coopsharetx
|
||||||
// this is just a workaround:
|
// this is just a workaround:
|
||||||
return getRawTableName()
|
return getRawTableName()
|
||||||
.replace("hs_office.", "hsof.")
|
.replace("hs_office.", "hsof.")
|
||||||
.replace("hs_booking_", "hsbk_")
|
.replace("hs_booking.", "hsbk_")
|
||||||
.replace("hs_hosting_", "hsho_")
|
.replace("hs_hosting.", "hsho_");
|
||||||
.replace("coopsharestransaction", "coopsharetx")
|
|
||||||
.replace("coopassetstransaction", "coopassettx");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String dependsOnColumName() {
|
String dependsOnColumName() {
|
||||||
|
@ -20,7 +20,6 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacView.RbacGrantDefinit
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacView.Role.*;
|
||||||
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
|
import static net.hostsharing.hsadminng.rbac.generator.StringWriter.with;
|
||||||
import static org.apache.commons.lang3.StringUtils.capitalize;
|
import static org.apache.commons.lang3.StringUtils.capitalize;
|
||||||
import static org.apache.commons.lang3.StringUtils.uncapitalize;
|
|
||||||
|
|
||||||
class RolesGrantsAndPermissionsGenerator {
|
class RolesGrantsAndPermissionsGenerator {
|
||||||
|
|
||||||
@ -362,11 +361,10 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
System.out.println("null");
|
System.out.println("null");
|
||||||
}
|
}
|
||||||
if (roleDef.getEntityAlias().isGlobal()) {
|
if (roleDef.getEntityAlias().isGlobal()) {
|
||||||
return "rbac.globalAdmin()";
|
return "rbac.global_ADMIN()";
|
||||||
}
|
}
|
||||||
final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias());
|
final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias());
|
||||||
return roleDef.getEntityAlias().simpleName() + capitalize(roleDef.getRole().name())
|
return roleDef.descriptorFunctionName() + "(" + entityRefVar + ")";
|
||||||
+ "(" + entityRefVar + ")";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private String entityRefVar(
|
private String entityRefVar(
|
||||||
@ -389,8 +387,8 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
plPgSql.writeLn();
|
plPgSql.writeLn();
|
||||||
plPgSql.writeLn("perform rbac.defineRoleWithGrants(");
|
plPgSql.writeLn("perform rbac.defineRoleWithGrants(");
|
||||||
plPgSql.indented(() -> {
|
plPgSql.indented(() -> {
|
||||||
plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW),"
|
plPgSql.writeLn("${qualifiedRawTableName)_${roleSuffix}(NEW),"
|
||||||
.replace("${simpleVarName)", simpleEntityVarName)
|
.replace("${qualifiedRawTableName)", qualifiedRawTableName)
|
||||||
.replace("${roleSuffix}", capitalize(role.name())));
|
.replace("${roleSuffix}", capitalize(role.name())));
|
||||||
|
|
||||||
generatePermissionsForRole(plPgSql, role);
|
generatePermissionsForRole(plPgSql, role);
|
||||||
@ -593,16 +591,12 @@ class RolesGrantsAndPermissionsGenerator {
|
|||||||
final RbacView.RbacRoleDefinition roleDef,
|
final RbacView.RbacRoleDefinition roleDef,
|
||||||
final boolean assumed) {
|
final boolean assumed) {
|
||||||
final var assumedArg = assumed ? "" : ", rbac.unassumed()";
|
final var assumedArg = assumed ? "" : ", rbac.unassumed()";
|
||||||
return toRoleRef(roleDef) +
|
return roleDef.descriptorFunctionName() +
|
||||||
(roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(rbac.unassumed())")
|
(roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(rbac.unassumed())")
|
||||||
: rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
|
: rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
|
||||||
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + assumedArg + ")");
|
: "(" + toTriggerReference(triggerRef, roleDef.getEntityAlias()) + assumedArg + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toRoleRef(final RbacView.RbacRoleDefinition roleDef) {
|
|
||||||
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().name());
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String toTriggerReference(
|
private static String toTriggerReference(
|
||||||
final PostgresTriggerReference triggerRef,
|
final PostgresTriggerReference triggerRef,
|
||||||
final RbacView.EntityAlias entityAlias) {
|
final RbacView.EntityAlias entityAlias) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.grant;
|
package net.hostsharing.hsadminng.rbac.grant;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacGrantsApi;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacGrantResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -22,7 +22,7 @@ public class RbacGrantController implements RbacGrantsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RbacGrantRepository rbacGrantRepository;
|
private RbacGrantRepository rbacGrantRepository;
|
||||||
|
@ -30,7 +30,7 @@ public class RbacGrantsDiagramService {
|
|||||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
|
||||||
writer.write("""
|
writer.write("""
|
||||||
### all grants to %s
|
### all grants to %s
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
%s
|
%s
|
||||||
```
|
```
|
||||||
@ -62,7 +62,7 @@ public class RbacGrantsDiagramService {
|
|||||||
@PersistenceContext
|
@PersistenceContext
|
||||||
private EntityManager em;
|
private EntityManager em;
|
||||||
|
|
||||||
private Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>();
|
private final Map<UUID, List<RawRbacGrantEntity>> descendantsByUuid = new HashMap<>();
|
||||||
|
|
||||||
public String allGrantsTocurrentSubject(final EnumSet<Include> includes) {
|
public String allGrantsTocurrentSubject(final EnumSet<Include> includes) {
|
||||||
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
||||||
@ -231,8 +231,7 @@ public class RbacGrantsDiagramService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
record Node(String idName, UUID uuid) {
|
||||||
|
|
||||||
record Node(String idName, UUID uuid) {
|
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.role;
|
package net.hostsharing.hsadminng.rbac.role;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacRolesApi;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -18,7 +18,7 @@ public class RbacRoleController implements RbacRolesApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RbacRoleRepository rbacRoleRepository;
|
private RbacRoleRepository rbacRoleRepository;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.subject;
|
package net.hostsharing.hsadminng.rbac.subject;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacSubjectsApi;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectPermissionResource;
|
||||||
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
|
||||||
@ -21,7 +21,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RbacSubjectRepository rbacSubjectRepository;
|
private RbacSubjectRepository rbacSubjectRepository;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test.cust;
|
package net.hostsharing.hsadminng.rbac.test.cust;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
|
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestCustomersApi;
|
||||||
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
|
import net.hostsharing.hsadminng.test.generated.api.v1.model.TestCustomerResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@ -21,7 +21,7 @@ public class TestCustomerController implements TestCustomersApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TestCustomerRepository testCustomerRepository;
|
private TestCustomerRepository testCustomerRepository;
|
||||||
|
@ -5,7 +5,7 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.rbac.test.pac.TestPackageEntity;
|
import net.hostsharing.hsadminng.rbac.test.pac.TestPackageEntity;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test.pac;
|
package net.hostsharing.hsadminng.rbac.test.pac;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
import net.hostsharing.hsadminng.mapper.StandardMapper;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi;
|
import net.hostsharing.hsadminng.test.generated.api.v1.api.TestPackagesApi;
|
||||||
@ -21,7 +21,7 @@ public class TestPackageController implements TestPackagesApi {
|
|||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Mapper mapper;
|
private StandardMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TestPackageRepository testPackageRepository;
|
private TestPackageRepository testPackageRepository;
|
||||||
|
@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import net.hostsharing.hsadminng.rbac.object.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView;
|
||||||
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
import net.hostsharing.hsadminng.rbac.generator.RbacView.SQL;
|
||||||
import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity;
|
import net.hostsharing.hsadminng.rbac.test.cust.TestCustomerEntity;
|
||||||
|
@ -10,6 +10,7 @@ components:
|
|||||||
- CLOUD_SERVER
|
- CLOUD_SERVER
|
||||||
- MANAGED_SERVER
|
- MANAGED_SERVER
|
||||||
- MANAGED_WEBSPACE
|
- MANAGED_WEBSPACE
|
||||||
|
- DOMAIN_SETUP
|
||||||
|
|
||||||
HsBookingItem:
|
HsBookingItem:
|
||||||
type: object
|
type: object
|
||||||
@ -55,6 +56,10 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
nullable: false
|
nullable: false
|
||||||
|
parentItemUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: false
|
||||||
type:
|
type:
|
||||||
$ref: '#/components/schemas/HsBookingItemType'
|
$ref: '#/components/schemas/HsBookingItemType'
|
||||||
caption:
|
caption:
|
||||||
@ -68,6 +73,8 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
resources:
|
resources:
|
||||||
$ref: '#/components/schemas/BookingResources'
|
$ref: '#/components/schemas/BookingResources'
|
||||||
|
hostingAsset:
|
||||||
|
$ref: '../hs-hosting/hs-hosting-asset-schemas.yaml#/components/schemas/HsHostingAssetAutoInsert'
|
||||||
required:
|
required:
|
||||||
- caption
|
- caption
|
||||||
- projectUuid
|
- projectUuid
|
||||||
|
@ -94,7 +94,68 @@ components:
|
|||||||
- type
|
- type
|
||||||
- identifier
|
- identifier
|
||||||
- caption
|
- caption
|
||||||
- config
|
additionalProperties: false
|
||||||
|
|
||||||
|
HsHostingAssetAutoInsert:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
parentAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
assignedToAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetType'
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
caption:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
alarmContactUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
config:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||||
|
subHostingAssets:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetSubInsert'
|
||||||
|
required:
|
||||||
|
- identifier
|
||||||
|
additionalProperties: false
|
||||||
|
|
||||||
|
HsHostingAssetSubInsert:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetType'
|
||||||
|
identifier:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
caption:
|
||||||
|
type: string
|
||||||
|
minLength: 3
|
||||||
|
maxLength: 80
|
||||||
|
nullable: false
|
||||||
|
assignedToAssetUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
alarmContactUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
config:
|
||||||
|
$ref: '#/components/schemas/HsHostingAssetConfiguration'
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
HsHostingAssetConfiguration:
|
HsHostingAssetConfiguration:
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
get:
|
get:
|
||||||
summary: Returns a list of (optionally filtered) person relations for a given person.
|
summary: Returns a list of (optionally filtered) person relations for a given person.
|
||||||
description: Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles.
|
description:
|
||||||
|
Returns the list of (optionally filtered) person relations of a given person and which are visible to the current subject or any of it's assumed roles.
|
||||||
|
To match data, all given query parameters must be fulfilled ('and' / logical conjunction).
|
||||||
tags:
|
tags:
|
||||||
- hs-office-relations
|
- hs-office-relations
|
||||||
operationId: listRelations
|
operationId: listRelations
|
||||||
@ -9,7 +11,7 @@ get:
|
|||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: personUuid
|
- name: personUuid
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
@ -20,6 +22,18 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
|
$ref: 'hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
|
||||||
description: Prefix of name properties from holder or contact to filter the results.
|
description: Prefix of name properties from holder or contact to filter the results.
|
||||||
|
- name: personData
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: 'Data from any of these text field in the anchor or holder person: tradeName, familyName, givenName'
|
||||||
|
- name: contactData
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: 'Data from any of these text field in the contact: caption, postalAddress, emailAddresses, phoneNumbers'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
|
@ -168,45 +168,6 @@ begin
|
|||||||
return cleanIdentifier;
|
return cleanIdentifier;
|
||||||
end; $$;
|
end; $$;
|
||||||
|
|
||||||
create or replace function base.findObjectUuidByIdName(objectTable varchar, objectIdName varchar)
|
|
||||||
returns uuid
|
|
||||||
returns null on null input
|
|
||||||
language plpgsql as $$
|
|
||||||
declare
|
|
||||||
sql varchar;
|
|
||||||
uuid uuid;
|
|
||||||
begin
|
|
||||||
objectTable := base.pureIdentifier(objectTable);
|
|
||||||
objectIdName := base.pureIdentifier(objectIdName);
|
|
||||||
sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName);
|
|
||||||
begin
|
|
||||||
execute sql into uuid;
|
|
||||||
exception
|
|
||||||
when others then
|
|
||||||
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
|
|
||||||
end;
|
|
||||||
return uuid;
|
|
||||||
end ; $$;
|
|
||||||
|
|
||||||
create or replace function base.findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
|
|
||||||
returns varchar
|
|
||||||
returns null on null input
|
|
||||||
language plpgsql as $$
|
|
||||||
declare
|
|
||||||
sql varchar;
|
|
||||||
idName varchar;
|
|
||||||
begin
|
|
||||||
objectTable := base.pureIdentifier(objectTable);
|
|
||||||
sql := format('select * from %sIdNameByUuid(%L::uuid);', objectTable, objectUuid);
|
|
||||||
begin
|
|
||||||
execute sql into idName;
|
|
||||||
exception
|
|
||||||
when others then
|
|
||||||
raise exception 'function %IdNameByUuid(...) not found, add identity view support for table %', objectTable, objectTable;
|
|
||||||
end;
|
|
||||||
return idName;
|
|
||||||
end ; $$;
|
|
||||||
|
|
||||||
create or replace function base.currentSubjects()
|
create or replace function base.currentSubjects()
|
||||||
returns varchar(1023)[]
|
returns varchar(1023)[]
|
||||||
stable -- leakproof
|
stable -- leakproof
|
||||||
|
@ -9,6 +9,9 @@ create or replace function base.combine_table_schema_and_name(tableSchema name,
|
|||||||
returns text
|
returns text
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
begin
|
begin
|
||||||
|
assert LEFT(tableSchema, 1) <> '"', 'tableSchema must not start with "';
|
||||||
|
assert LEFT(tableName, 1) <> '"', 'tableName must not start with "';
|
||||||
|
|
||||||
if tableSchema is null or tableSchema = 'public' or tableSchema = '' then
|
if tableSchema is null or tableSchema = 'public' or tableSchema = '' then
|
||||||
return tableName::text;
|
return tableName::text;
|
||||||
else
|
else
|
||||||
|
@ -63,7 +63,6 @@ begin
|
|||||||
if (currentSubject is null or currentSubject = '') then
|
if (currentSubject is null or currentSubject = '') then
|
||||||
raise exception 'hsadminng.currentSubject must be defined, please use "SET LOCAL ...;"';
|
raise exception 'hsadminng.currentSubject must be defined, please use "SET LOCAL ...;"';
|
||||||
end if;
|
end if;
|
||||||
raise notice 'currentSubject: %', currentSubject;
|
|
||||||
|
|
||||||
-- determine task
|
-- determine task
|
||||||
currentTask = current_setting('hsadminng.currentTask');
|
currentTask = current_setting('hsadminng.currentTask');
|
||||||
@ -81,8 +80,9 @@ begin
|
|||||||
"alive" := false;
|
"alive" := false;
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
sql := format('INSERT INTO %3$I_ex VALUES (DEFAULT, pg_current_xact_id(), %1$L, %2$L, $1.*)',
|
sql := format('INSERT INTO %3$s_ex VALUES (DEFAULT, pg_current_xact_id(), %1$L, %2$L, $1.*)',
|
||||||
TG_OP, alive, base.combine_table_schema_and_name(tg_table_schema, tg_table_name)::name);
|
TG_OP, alive, base.combine_table_schema_and_name(tg_table_schema, tg_table_name)::name);
|
||||||
|
-- raise exception 'generated-SQL: %', sql;
|
||||||
execute sql using "row";
|
execute sql using "row";
|
||||||
|
|
||||||
return "row";
|
return "row";
|
||||||
@ -117,12 +117,12 @@ begin
|
|||||||
' EXCLUDING CONSTRAINTS' ||
|
' EXCLUDING CONSTRAINTS' ||
|
||||||
' EXCLUDING STATISTICS' ||
|
' EXCLUDING STATISTICS' ||
|
||||||
')';
|
')';
|
||||||
raise notice 'sql: %', createHistTableSql;
|
-- raise notice 'sql: %', createHistTableSql;
|
||||||
execute createHistTableSql;
|
execute createHistTableSql;
|
||||||
|
|
||||||
-- create the historical view
|
-- create the historical view
|
||||||
viewName = quote_ident(format('%s_hv', baseTable));
|
viewName = baseTable || '_hv';
|
||||||
exVersionsTable = quote_ident(format('%s_ex', baseTable));
|
exVersionsTable = baseTable || '_ex';
|
||||||
baseCols = (select string_agg(quote_ident(column_name), ', ')
|
baseCols = (select string_agg(quote_ident(column_name), ', ')
|
||||||
from information_schema.columns
|
from information_schema.columns
|
||||||
where table_schema = 'public'
|
where table_schema = 'public'
|
||||||
@ -146,15 +146,14 @@ begin
|
|||||||
' )' ||
|
' )' ||
|
||||||
')',
|
')',
|
||||||
viewName, baseCols, exVersionsTable
|
viewName, baseCols, exVersionsTable
|
||||||
);
|
);
|
||||||
raise notice 'sql: %', createViewSQL;
|
-- raise notice 'generated-sql: %', createViewSQL;
|
||||||
execute createViewSQL;
|
execute createViewSQL;
|
||||||
|
|
||||||
-- "-9-" to put the trigger execution after any alphabetically lesser tx-triggers
|
-- "-9-" to put the trigger execution after any alphabetically lesser tx-triggers
|
||||||
createTriggerSQL = 'CREATE TRIGGER tx_9_historicize_tg' ||
|
createTriggerSQL = 'CREATE TRIGGER tx_9_historicize_tg' ||
|
||||||
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
' AFTER INSERT OR DELETE OR UPDATE ON ' || baseTable ||
|
||||||
' FOR EACH ROW EXECUTE PROCEDURE base.tx_historicize_tf()';
|
' FOR EACH ROW EXECUTE PROCEDURE base.tx_historicize_tf()';
|
||||||
raise notice 'sql: %', createTriggerSQL;
|
|
||||||
execute createTriggerSQL;
|
execute createTriggerSQL;
|
||||||
|
|
||||||
end; $$;
|
end; $$;
|
||||||
|
@ -233,6 +233,50 @@ $$;
|
|||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
--changeset michael.hoennig:rbac-base-IDNAME-FUNCTIONS endDelimiter:--//
|
||||||
|
-- ----------------------------------------------------------------------------
|
||||||
|
create or replace function rbac.findObjectUuidByIdName(objectTable varchar, objectIdName varchar)
|
||||||
|
returns uuid
|
||||||
|
returns null on null input
|
||||||
|
language plpgsql as $$
|
||||||
|
declare
|
||||||
|
sql varchar;
|
||||||
|
uuid uuid;
|
||||||
|
begin
|
||||||
|
objectTable := base.pureIdentifier(objectTable);
|
||||||
|
objectIdName := base.pureIdentifier(objectIdName);
|
||||||
|
sql := format('select * from %s_uuid_by_id_name(%L);', objectTable, objectIdName);
|
||||||
|
begin
|
||||||
|
execute sql into uuid;
|
||||||
|
exception
|
||||||
|
when others then
|
||||||
|
raise exception 'function %_uuid_by_id_name(...) not found, add identity view support for table %', objectTable, objectTable;
|
||||||
|
end;
|
||||||
|
return uuid;
|
||||||
|
end ; $$;
|
||||||
|
|
||||||
|
create or replace function rbac.findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
|
||||||
|
returns varchar
|
||||||
|
returns null on null input
|
||||||
|
language plpgsql as $$
|
||||||
|
declare
|
||||||
|
sql varchar;
|
||||||
|
idName varchar;
|
||||||
|
begin
|
||||||
|
objectTable := base.pureIdentifier(objectTable);
|
||||||
|
sql := format('select * from %s_id_name_by_uuid(%L::uuid);', objectTable, objectUuid);
|
||||||
|
begin
|
||||||
|
execute sql into idName;
|
||||||
|
exception
|
||||||
|
when others then
|
||||||
|
raise exception 'function %_id_name_by_uuid(...) not found, add identity view support for table %', objectTable, objectTable;
|
||||||
|
end;
|
||||||
|
return idName;
|
||||||
|
end ; $$;
|
||||||
|
--//
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:rbac-base-ROLE-FUNCTIONS endDelimiter:--//
|
--changeset michael.hoennig:rbac-base-ROLE-FUNCTIONS endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
@ -262,7 +306,7 @@ begin
|
|||||||
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
|
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
|
||||||
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
||||||
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
||||||
objectUuidOfRole = base.findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName);
|
objectUuidOfRole = rbac.findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName);
|
||||||
|
|
||||||
select uuid
|
select uuid
|
||||||
from rbac.role
|
from rbac.role
|
||||||
|
@ -55,7 +55,7 @@ begin
|
|||||||
objectNameToAssume = split_part(roleNameParts, '#', 2);
|
objectNameToAssume = split_part(roleNameParts, '#', 2);
|
||||||
roleTypeToAssume = split_part(roleNameParts, '#', 3);
|
roleTypeToAssume = split_part(roleNameParts, '#', 3);
|
||||||
|
|
||||||
objectUuidToAssume = base.findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
|
objectUuidToAssume = rbac.findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
|
||||||
if objectUuidToAssume is null then
|
if objectUuidToAssume is null then
|
||||||
raise exception '[401] object % cannot be found in table % (from roleNameParts=%)', objectNameToAssume, objectTableToAssume, roleNameParts;
|
raise exception '[401] object % cannot be found in table % (from roleNameParts=%)', objectNameToAssume, objectTableToAssume, roleNameParts;
|
||||||
end if;
|
end if;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user