Merge remote-tracking branch 'origin/master' into add-salut-and-title-to-person
This commit is contained in:
@@ -15,11 +15,9 @@ import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresArray.fromPostgresArray;
|
||||
import static org.springframework.transaction.annotation.Propagation.MANDATORY;
|
||||
|
||||
@Service
|
||||
@@ -55,16 +53,15 @@ public class Context {
|
||||
final String currentRequest,
|
||||
final String currentUser,
|
||||
final String assumedRoles) {
|
||||
final var query = em.createNativeQuery(
|
||||
"""
|
||||
call defineContext(
|
||||
cast(:currentTask as varchar),
|
||||
cast(:currentRequest as varchar),
|
||||
cast(:currentUser as varchar),
|
||||
cast(:assumedRoles as varchar));
|
||||
""");
|
||||
query.setParameter("currentTask", shortenToMaxLength(currentTask, 96));
|
||||
query.setParameter("currentRequest", shortenToMaxLength(currentRequest, 512)); // TODO.spec: length?
|
||||
final var query = em.createNativeQuery("""
|
||||
call defineContext(
|
||||
cast(:currentTask as varchar(127)),
|
||||
cast(:currentRequest as text),
|
||||
cast(:currentUser as varchar(63)),
|
||||
cast(:assumedRoles as varchar(1023)));
|
||||
""");
|
||||
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127));
|
||||
query.setParameter("currentRequest", currentRequest);
|
||||
query.setParameter("currentUser", currentUser);
|
||||
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
|
||||
query.executeUpdate();
|
||||
@@ -83,14 +80,11 @@ public class Context {
|
||||
}
|
||||
|
||||
public String[] getAssumedRoles() {
|
||||
final byte[] result = (byte[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
|
||||
return fromPostgresArray(result, String.class, Function.identity());
|
||||
return (String[]) em.createNativeQuery("select assumedRoles() as roles", String[].class).getSingleResult();
|
||||
}
|
||||
|
||||
public UUID[] currentSubjectsUuids() {
|
||||
final byte[] result = (byte[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class)
|
||||
.getSingleResult();
|
||||
return fromPostgresArray(result, UUID.class, UUID::fromString);
|
||||
return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
|
||||
}
|
||||
|
||||
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.errors;
|
||||
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException {
|
||||
|
||||
private final Class<?> entityClass;
|
||||
private final UUID uuid;
|
||||
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
|
||||
public <E extends RbacObject> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
|
||||
super(exc);
|
||||
this.entityClass = entityClass;
|
||||
this.uuid = uuid;
|
||||
|
@@ -11,16 +11,18 @@ import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.orm.jpa.JpaObjectRetrievalFailureException;
|
||||
import org.springframework.orm.jpa.JpaSystemException;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.validation.method.ParameterValidationResult;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
||||
@@ -119,6 +121,28 @@ public class RestResponseEntityExceptionHandler
|
||||
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked,rawtypes")
|
||||
|
||||
@Override
|
||||
protected ResponseEntity handleHandlerMethodValidationException(
|
||||
final HandlerMethodValidationException exc,
|
||||
final HttpHeaders headers,
|
||||
final HttpStatusCode status,
|
||||
final WebRequest request) {
|
||||
final var errorList = exc
|
||||
.getAllValidationResults()
|
||||
.stream()
|
||||
.map(ParameterValidationResult::getResolvableErrors)
|
||||
.flatMap(Collection::stream)
|
||||
.filter(FieldError.class::isInstance)
|
||||
.map(FieldError.class::cast)
|
||||
.map(fieldError -> fieldError.getField() + " " + fieldError.getDefaultMessage() + " but is \""
|
||||
+ fieldError.getRejectedValue() + "\"")
|
||||
.toList();
|
||||
return errorResponse(request, HttpStatus.BAD_REQUEST, errorList.toString());
|
||||
}
|
||||
|
||||
|
||||
private String userReadableEntityClassName(final String exceptionMessage) {
|
||||
final var regex = "(net.hostsharing.hsadminng.[a-z0-9_.]*.[A-Za-z0-9_$]*Entity) ";
|
||||
final var pattern = Pattern.compile(regex);
|
||||
|
@@ -3,7 +3,8 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
@@ -11,8 +12,13 @@ import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -24,11 +30,11 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("BankAccount")
|
||||
public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
|
||||
public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
|
||||
.withIdProp(HsOfficeBankAccountEntity::getIban)
|
||||
.withProp(Fields.holder, HsOfficeBankAccountEntity::getHolder)
|
||||
.withProp(Fields.iban, HsOfficeBankAccountEntity::getIban)
|
||||
.withProp(Fields.bic, HsOfficeBankAccountEntity::getBic);
|
||||
|
||||
@Id
|
||||
@@ -50,4 +56,28 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
|
||||
public String toShortString() {
|
||||
return holder;
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class)
|
||||
.withIdentityView(SQL.projection("iban"))
|
||||
.withUpdatableColumns("holder", "iban", "bic")
|
||||
|
||||
.toRole("global", GUEST).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
with.permission(DELETE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/505-bankaccount/5053-hs-office-bankaccount-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.contact;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -22,13 +30,12 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Contact")
|
||||
public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
||||
public class HsOfficeContactEntity implements Stringifyable, RbacObject {
|
||||
|
||||
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
|
||||
.withProp(Fields.label, HsOfficeContactEntity::getLabel)
|
||||
.withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses);
|
||||
|
||||
|
||||
@Id
|
||||
@GeneratedValue(generator = "UUID")
|
||||
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
|
||||
@@ -36,13 +43,13 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
||||
private String label;
|
||||
|
||||
@Column(name = "postaladdress")
|
||||
private String postalAddress; // TODO: check if we really want multiple, if so: JSON-Array or Postgres-Array?
|
||||
private String postalAddress; // TODO.spec: check if we really want multiple, if so: JSON-Array or Postgres-Array?
|
||||
|
||||
@Column(name = "emailaddresses", columnDefinition = "json")
|
||||
private String emailAddresses; // TODO: check if we can really add multiple. format: ["eins@...", "zwei@..."]
|
||||
private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."]
|
||||
|
||||
@Column(name = "phonenumbers", columnDefinition = "json")
|
||||
private String phoneNumbers; // TODO: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
|
||||
private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@@ -53,4 +60,26 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid {
|
||||
public String toShortString() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("contact", HsOfficeContactEntity.class)
|
||||
.withIdentityView(SQL.projection("label"))
|
||||
.withUpdatableColumns("label", "postalAddress", "emailAddresses", "phoneNumbers")
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
with.permission(DELETE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
})
|
||||
.toRole(GLOBAL, GUEST).grantPermission(INSERT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/501-contact/5013-hs-office-contact-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
@@ -59,7 +58,7 @@ public class HsOfficeCoopAssetsTransactionController implements HsOfficeCoopAsse
|
||||
public ResponseEntity<HsOfficeCoopAssetsTransactionResource> addCoopAssetsTransaction(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
@Valid final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
||||
final HsOfficeCoopAssetsTransactionInsertResource requestBody) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
validate(requestBody);
|
||||
|
@@ -1,20 +1,47 @@
|
||||
|
||||
package net.hostsharing.hsadminng.hs.office.coopassets;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
import java.io.IOException;
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -25,16 +52,15 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("CoopAssetsTransaction")
|
||||
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUuid {
|
||||
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject {
|
||||
|
||||
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getMemberNumber)
|
||||
.withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getValueDate)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getTransactionType)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getAssetValue)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopAssetsTransactionEntity::getComment)
|
||||
.withSeparator(", ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -76,8 +102,8 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
|
||||
private String comment;
|
||||
|
||||
|
||||
public Integer getMemberNumber() {
|
||||
return ofNullable(membership).map(HsOfficeMembershipEntity::getMemberNumber).orElse(null);
|
||||
public String getTaggedMemberNumber() {
|
||||
return ofNullable(membership).map(HsOfficeMembershipEntity::toShortString).orElse("M-?????");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,6 +113,24 @@ public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUu
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return "%s%+1.2f".formatted(getMemberNumber(), assetValue);
|
||||
return "%s:%+1.2f".formatted(getTaggedMemberNumber(), Optional.ofNullable(assetValue).orElse(BigDecimal.ZERO));
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("coopAssetsTransaction", HsOfficeCoopAssetsTransactionEntity.class)
|
||||
.withIdentityView(RbacView.SQL.projection("reference"))
|
||||
.withUpdatableColumns("comment")
|
||||
.importEntityAlias("membership", HsOfficeMembershipEntity.class,
|
||||
dependsOnColumn("membershipUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
.toRole("membership", ADMIN).grantPermission(INSERT)
|
||||
.toRole("membership", ADMIN).grantPermission(UPDATE)
|
||||
.toRole("membership", AGENT).grantPermission(SELECT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/512-coopassets/5123-hs-office-coopassets-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.ValidationException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
@@ -60,7 +59,7 @@ public class HsOfficeCoopSharesTransactionController implements HsOfficeCoopShar
|
||||
public ResponseEntity<HsOfficeCoopSharesTransactionResource> addCoopSharesTransaction(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
@Valid final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
||||
final HsOfficeCoopSharesTransactionInsertResource requestBody) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
validate(requestBody);
|
||||
|
@@ -1,17 +1,41 @@
|
||||
package net.hostsharing.hsadminng.hs.office.coopshares;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.UPDATE;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -22,7 +46,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("CoopShareTransaction")
|
||||
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
|
||||
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject {
|
||||
|
||||
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)
|
||||
@@ -31,7 +55,6 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getShareCount)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getReference)
|
||||
.withProp(HsOfficeCoopSharesTransactionEntity::getComment)
|
||||
.withSeparator(", ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -84,4 +107,22 @@ public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUu
|
||||
public String toShortString() {
|
||||
return "%s%+d".formatted(getMemberNumberTagged(), shareCount);
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("coopSharesTransaction", HsOfficeCoopSharesTransactionEntity.class)
|
||||
.withIdentityView(SQL.projection("reference"))
|
||||
.withUpdatableColumns("comment")
|
||||
.importEntityAlias("membership", HsOfficeMembershipEntity.class,
|
||||
dependsOnColumn("membershipUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
.toRole("membership", ADMIN).grantPermission(INSERT)
|
||||
.toRole("membership", ADMIN).grantPermission(UPDATE)
|
||||
.toRole("membership", AGENT).grantPermission(SELECT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/511-coopshares/5113-hs-office-coopshares-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -5,7 +5,11 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeDebitors
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorResource;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -13,10 +17,13 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.DEBITOR;
|
||||
|
||||
@RestController
|
||||
|
||||
public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
@@ -30,6 +37,9 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
@Autowired
|
||||
private HsOfficeDebitorRepository debitorRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationRepository relRepo;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@@ -53,22 +63,44 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<HsOfficeDebitorResource> addDebitor(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final HsOfficeDebitorInsertResource body) {
|
||||
String currentUser,
|
||||
String assumedRoles,
|
||||
HsOfficeDebitorInsertResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRelUuid() == null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found both");
|
||||
Validate.isTrue(body.getDebitorRel() != null || body.getDebitorRelUuid() != null,
|
||||
"ERROR: [400] exactly one of debitorRel and debitorRelUuid must be supplied, but found none");
|
||||
Validate.isTrue(body.getDebitorRel() == null ||
|
||||
body.getDebitorRel().getType() == null || DEBITOR.name().equals(body.getDebitorRel().getType()),
|
||||
"ERROR: [400] debitorRel.type must be '"+DEBITOR.name()+"' or null for default");
|
||||
Validate.isTrue(body.getDebitorRel() == null || body.getDebitorRel().getMark() == null,
|
||||
"ERROR: [400] debitorRel.mark must be null");
|
||||
|
||||
final var saved = debitorRepo.save(entityToSave);
|
||||
final var entityToSave = mapper.map(body, HsOfficeDebitorEntity.class);
|
||||
if ( body.getDebitorRel() != null ) {
|
||||
body.getDebitorRel().setType(DEBITOR.name());
|
||||
final var debitorRel = mapper.map(body.getDebitorRel(), HsOfficeRelationEntity.class);
|
||||
entityToSave.setDebitorRel(relRepo.save(debitorRel));
|
||||
} else {
|
||||
final var debitorRelOptional = relRepo.findByUuid(body.getDebitorRelUuid());
|
||||
debitorRelOptional.ifPresentOrElse(
|
||||
debitorRel -> {entityToSave.setDebitorRel(relRepo.save(debitorRel));},
|
||||
() -> { throw new EntityNotFoundException("ERROR: [400] debitorRelUuid not found: " + body.getDebitorRelUuid());});
|
||||
}
|
||||
|
||||
final var savedEntity = debitorRepo.save(entityToSave);
|
||||
em.flush();
|
||||
em.refresh(savedEntity);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/office/debitors/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.buildAndExpand(savedEntity.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||
final var mapped = mapper.map(savedEntity, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@@ -119,6 +151,7 @@ public class HsOfficeDebitorController implements HsOfficeDebitorsApi {
|
||||
new HsOfficeDebitorEntityPatcher(em, current).apply(body);
|
||||
|
||||
final var saved = debitorRepo.save(current);
|
||||
Hibernate.initialize(saved);
|
||||
final var mapped = mapper.map(saved, HsOfficeDebitorResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
@@ -3,40 +3,56 @@ package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
import org.hibernate.annotations.JoinFormula;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.Optional;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static jakarta.persistence.CascadeType.DETACH;
|
||||
import static jakarta.persistence.CascadeType.MERGE;
|
||||
import static jakarta.persistence.CascadeType.PERSIST;
|
||||
import static jakarta.persistence.CascadeType.REFRESH;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NULLABLE;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(name = "hs_office_debitor_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@Builder(toBuilder = true)
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Debitor")
|
||||
public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
|
||||
|
||||
public static final String DEBITOR_NUMBER_TAG = "D-";
|
||||
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||
|
||||
// TODO: I would rather like to generate something matching this example:
|
||||
// debitor(1234500: Test AG, tes)
|
||||
// maybe remove withSepararator (always use ', ') and add withBusinessIdProp (with ': ' afterwards)?
|
||||
private static Stringify<HsOfficeDebitorEntity> stringify =
|
||||
stringify(HsOfficeDebitorEntity.class, "debitor")
|
||||
.withProp(e -> DEBITOR_NUMBER_TAG + e.getDebitorNumber())
|
||||
.withProp(HsOfficeDebitorEntity::getPartner)
|
||||
.withIdProp(HsOfficeDebitorEntity::toShortString)
|
||||
.withProp(e -> ofNullable(e.getDebitorRel()).map(HsOfficeRelationEntity::toShortString).orElse(null))
|
||||
.withProp(HsOfficeDebitorEntity::getDefaultPrefix)
|
||||
.withSeparator(": ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -45,15 +61,29 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
private UUID uuid;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "partneruuid")
|
||||
@JoinFormula(
|
||||
referencedColumnName = "uuid",
|
||||
value = """
|
||||
(
|
||||
SELECT DISTINCT partner.uuid
|
||||
FROM hs_office_partner_rv partner
|
||||
JOIN hs_office_relation_rv dRel
|
||||
ON dRel.uuid = debitorreluuid AND dRel.type = 'DEBITOR'
|
||||
JOIN hs_office_relation_rv pRel
|
||||
ON pRel.uuid = partner.partnerRelUuid AND pRel.type = 'PARTNER'
|
||||
WHERE pRel.holderUuid = dRel.anchorUuid
|
||||
)
|
||||
""")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
@Column(name = "debitornumbersuffix", columnDefinition = "numeric(2)")
|
||||
private Byte debitorNumberSuffix; // TODO maybe rather as a formatted String?
|
||||
@Column(name = "debitornumbersuffix", length = 2)
|
||||
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||
private String debitorNumberSuffix;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "billingcontactuuid")
|
||||
private HsOfficeContactEntity billingContact; // TODO: migrate to billingPerson
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
|
||||
@JoinColumn(name = "debitorreluuid", nullable = false)
|
||||
private HsOfficeRelationEntity debitorRel;
|
||||
|
||||
@Column(name = "billable", nullable = false)
|
||||
private Boolean billable; // not a primitive because otherwise the default would be false
|
||||
@@ -78,14 +108,16 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
private String defaultPrefix;
|
||||
|
||||
private String getDebitorNumberString() {
|
||||
if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) {
|
||||
return null;
|
||||
}
|
||||
return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix);
|
||||
return ofNullable(partner)
|
||||
.filter(partner -> debitorNumberSuffix != null)
|
||||
.map(HsOfficePartnerEntity::getPartnerNumber)
|
||||
.map(Object::toString)
|
||||
.map(partnerNumber -> partnerNumber + debitorNumberSuffix)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
public Integer getDebitorNumber() {
|
||||
return Optional.ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
||||
return ofNullable(getDebitorNumberString()).map(Integer::parseInt).orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,4 +129,68 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
|
||||
public String toShortString() {
|
||||
return DEBITOR_NUMBER_TAG + getDebitorNumberString();
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("debitor", HsOfficeDebitorEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT debitor.uuid AS uuid,
|
||||
'D-' || (SELECT partner.partnerNumber
|
||||
FROM hs_office_partner partner
|
||||
JOIN hs_office_relation partnerRel
|
||||
ON partnerRel.uuid = partner.partnerRelUUid AND partnerRel.type = 'PARTNER'
|
||||
JOIN hs_office_relation debitorRel
|
||||
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
|
||||
WHERE debitorRel.uuid = debitor.debitorRelUuid)
|
||||
|| debitorNumberSuffix as idName
|
||||
FROM hs_office_debitor AS debitor
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
|
||||
.withUpdatableColumns(
|
||||
"debitorRelUuid",
|
||||
"billable",
|
||||
"refundBankAccountUuid",
|
||||
"vatId",
|
||||
"vatCountryCode",
|
||||
"vatBusiness",
|
||||
"vatReverseCharge",
|
||||
"defaultPrefix" /* TODO.spec: do we want that updatable? */)
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class,
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("debitorRelUuid"))
|
||||
.createPermission(DELETE).grantedTo("debitorRel", OWNER)
|
||||
.createPermission(UPDATE).grantedTo("debitorRel", ADMIN)
|
||||
.createPermission(SELECT).grantedTo("debitorRel", TENANT)
|
||||
|
||||
.importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class,
|
||||
dependsOnColumn("refundBankAccountUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NULLABLE)
|
||||
.toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT)
|
||||
.toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER)
|
||||
|
||||
.importEntityAlias("partnerRel", HsOfficeRelationEntity.class,
|
||||
dependsOnColumn("debitorRelUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office_relation AS partnerRel
|
||||
JOIN hs_office_relation AS debitorRel
|
||||
ON debitorRel.type = 'DEBITOR' AND debitorRel.anchorUuid = partnerRel.holderUuid
|
||||
WHERE partnerRel.type = 'PARTNER'
|
||||
AND ${REF}.debitorRelUuid = debitorRel.uuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
.toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN)
|
||||
.toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT)
|
||||
.toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT)
|
||||
.declarePlaceholderEntityAliases("partnerPerson", "operationalPerson")
|
||||
.forExampleRole("partnerPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
|
||||
.forExampleRole("operationalPerson", ADMIN).wouldBeGrantedTo("partnerRel", ADMIN)
|
||||
.forExampleRole("partnerRel", TENANT).wouldBeGrantedTo("partnerPerson", REFERRER);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/506-debitor/5063-hs-office-debitor-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package net.hostsharing.hsadminng.hs.office.debitor;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeDebitorPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
@@ -23,9 +23,9 @@ class HsOfficeDebitorEntityPatcher implements EntityPatcher<HsOfficeDebitorPatch
|
||||
|
||||
@Override
|
||||
public void apply(final HsOfficeDebitorPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getBillingContactUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "billingContact");
|
||||
entity.setBillingContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
||||
OptionalFromJson.of(resource.getDebitorRelUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "debitorRel");
|
||||
entity.setDebitorRel(em.getReference(HsOfficeRelationEntity.class, newValue));
|
||||
});
|
||||
Optional.ofNullable(resource.getBillable()).ifPresent(entity::setBillable);
|
||||
OptionalFromJson.of(resource.getVatId()).ifPresent(entity::setVatId);
|
||||
|
@@ -13,7 +13,10 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
WHERE cast(debitor.partner.partnerNumber as integer) = :partnerNumber
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
WHERE cast(partner.partnerNumber as integer) = :partnerNumber
|
||||
AND cast(debitor.debitorNumberSuffix as integer) = :debitorNumberSuffix
|
||||
""")
|
||||
List<HsOfficeDebitorEntity> findDebitorByDebitorNumber(int partnerNumber, byte debitorNumberSuffix);
|
||||
@@ -24,9 +27,15 @@ public interface HsOfficeDebitorRepository extends Repository<HsOfficeDebitorEnt
|
||||
|
||||
@Query("""
|
||||
SELECT debitor FROM HsOfficeDebitorEntity debitor
|
||||
JOIN HsOfficePartnerEntity partner ON partner.uuid = debitor.partner.uuid
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = partner.person.uuid
|
||||
JOIN HsOfficeContactEntity contact ON contact.uuid = debitor.billingContact.uuid
|
||||
JOIN HsOfficePartnerEntity partner
|
||||
ON partner.partnerRel.holder = debitor.debitorRel.anchor
|
||||
AND partner.partnerRel.type = 'PARTNER' AND debitor.debitorRel.type = 'DEBITOR'
|
||||
JOIN HsOfficePersonEntity person
|
||||
ON person.uuid = partner.partnerRel.holder.uuid
|
||||
OR person.uuid = debitor.debitorRel.holder.uuid
|
||||
JOIN HsOfficeContactEntity contact
|
||||
ON contact.uuid = debitor.debitorRel.contact.uuid
|
||||
OR contact.uuid = partner.partnerRel.contact.uuid
|
||||
WHERE :name is null
|
||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||
OR person.tradeName like concat(cast(:name as text), '%')
|
||||
|
@@ -12,9 +12,6 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -32,9 +29,6 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
@Autowired
|
||||
private HsOfficeMembershipRepository membershipRepo;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<HsOfficeMembershipResource>> listMemberships(
|
||||
@@ -58,7 +52,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
public ResponseEntity<HsOfficeMembershipResource> addMembership(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
@Valid final HsOfficeMembershipInsertResource body) {
|
||||
final HsOfficeMembershipInsertResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
@@ -121,7 +115,7 @@ public class HsOfficeMembershipController implements HsOfficeMembershipsApi {
|
||||
|
||||
final var current = membershipRepo.findByUuid(membershipUuid).orElseThrow();
|
||||
|
||||
new HsOfficeMembershipEntityPatcher(em, mapper, current).apply(body);
|
||||
new HsOfficeMembershipEntityPatcher(mapper, current).apply(body);
|
||||
|
||||
final var saved = membershipRepo.save(current);
|
||||
final var mapped = mapper.map(saved, HsOfficeMembershipResource.class, SEPA_MANDATE_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
|
@@ -1,23 +1,38 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
|
||||
import com.vladmihalcea.hibernate.type.range.Range;
|
||||
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
|
||||
import io.hypersistence.utils.hibernate.type.range.Range;
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Fetch;
|
||||
import org.hibernate.annotations.FetchMode;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.ADMIN;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.AGENT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.OWNER;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.TENANT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -28,17 +43,16 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Membership")
|
||||
public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
|
||||
public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
|
||||
|
||||
public static final String MEMBER_NUMBER_TAG = "M-";
|
||||
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
|
||||
|
||||
private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
|
||||
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
|
||||
.withProp(e -> e.getPartner().toShortString())
|
||||
.withProp(e -> e.getMainDebitor().toShortString())
|
||||
.withProp(e -> e.getValidity().asString())
|
||||
.withProp(HsOfficeMembershipEntity::getReasonForTermination)
|
||||
.withSeparator(", ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -49,12 +63,8 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
|
||||
@JoinColumn(name = "partneruuid")
|
||||
private HsOfficePartnerEntity partner;
|
||||
|
||||
@ManyToOne
|
||||
@Fetch(FetchMode.JOIN)
|
||||
@JoinColumn(name = "maindebitoruuid")
|
||||
private HsOfficeDebitorEntity mainDebitor;
|
||||
|
||||
@Column(name = "membernumbersuffix", length = 2)
|
||||
@Pattern(regexp = TWO_DECIMAL_DIGITS)
|
||||
private String memberNumberSuffix;
|
||||
|
||||
@Column(name = "validity", columnDefinition = "daterange")
|
||||
@@ -114,4 +124,45 @@ public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
|
||||
setReasonForTermination(HsOfficeReasonForTermination.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("membership", HsOfficeMembershipEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT m.uuid AS uuid,
|
||||
'M-' || p.partnerNumber || m.memberNumberSuffix as idName
|
||||
FROM hs_office_membership AS m
|
||||
JOIN hs_office_partner AS p ON p.uuid = m.partnerUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.projection("validity"))
|
||||
.withUpdatableColumns("validity", "membershipFeeBillable", "reasonForTermination")
|
||||
|
||||
.importEntityAlias("partnerRel", HsOfficeRelationEntity.class,
|
||||
dependsOnColumn("partnerUuid"),
|
||||
fetchedBySql("""
|
||||
SELECT ${columns}
|
||||
FROM hs_office_partner AS partner
|
||||
JOIN hs_office_relation AS partnerRel ON partnerRel.uuid = partner.partnerRelUuid
|
||||
WHERE partner.uuid = ${REF}.partnerUuid
|
||||
"""),
|
||||
NOT_NULL)
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.incomingSuperRole("partnerRel", ADMIN);
|
||||
with.permission(DELETE);
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT, (with) -> {
|
||||
with.incomingSuperRole("partnerRel", AGENT);
|
||||
with.outgoingSubRole("partnerRel", TENANT);
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/510-membership/5103-hs-office-membership-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,37 +1,26 @@
|
||||
package net.hostsharing.hsadminng.hs.office.membership;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeMembershipPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMembershipPatchResource> {
|
||||
|
||||
private final EntityManager em;
|
||||
private final Mapper mapper;
|
||||
private final HsOfficeMembershipEntity entity;
|
||||
|
||||
public HsOfficeMembershipEntityPatcher(
|
||||
final EntityManager em,
|
||||
final Mapper mapper,
|
||||
final HsOfficeMembershipEntity entity) {
|
||||
this.em = em;
|
||||
this.mapper = mapper;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(final HsOfficeMembershipPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getMainDebitorUuid())
|
||||
.ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "debitor");
|
||||
entity.setMainDebitor(em.getReference(HsOfficeDebitorEntity.class, newValue));
|
||||
});
|
||||
OptionalFromJson.of(resource.getValidTo()).ifPresent(
|
||||
entity::setValidTo);
|
||||
Optional.ofNullable(resource.getReasonForTermination())
|
||||
@@ -40,10 +29,4 @@ public class HsOfficeMembershipEntityPatcher implements EntityPatcher<HsOfficeMe
|
||||
OptionalFromJson.of(resource.getMembershipFeeBillable()).ifPresent(
|
||||
entity::setMembershipFeeBillable);
|
||||
}
|
||||
|
||||
private void verifyNotNull(final UUID newValue, final String propertyName) {
|
||||
if (newValue == null) {
|
||||
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,13 +7,13 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficePartners
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRoleInsertResource;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerRelInsertResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -40,7 +40,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private HsOfficePartnerRepository partnerRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationshipRepository relationshipRepo;
|
||||
private HsOfficeRelationRepository relationRepo;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
@@ -110,9 +110,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (partnerRepo.deleteByUuid(partnerUuid) != 1 ||
|
||||
// TODO: move to after delete trigger in partner
|
||||
relationshipRepo.deleteByUuid(partnerToDelete.get().getPartnerRole().getUuid()) != 1 ) {
|
||||
if (partnerRepo.deleteByUuid(partnerUuid) != 1) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
@@ -141,24 +139,22 @@ public class HsOfficePartnerController implements HsOfficePartnersApi {
|
||||
private HsOfficePartnerEntity createPartnerEntity(final HsOfficePartnerInsertResource body) {
|
||||
final var entityToSave = new HsOfficePartnerEntity();
|
||||
entityToSave.setPartnerNumber(body.getPartnerNumber());
|
||||
entityToSave.setPartnerRole(persistPartnerRole(body.getPartnerRole()));
|
||||
entityToSave.setContact(ref(HsOfficeContactEntity.class, body.getContactUuid()));
|
||||
entityToSave.setPerson(ref(HsOfficePersonEntity.class, body.getPersonUuid()));
|
||||
entityToSave.setPartnerRel(persistPartnerRel(body.getPartnerRel()));
|
||||
entityToSave.setDetails(mapper.map(body.getDetails(), HsOfficePartnerDetailsEntity.class));
|
||||
return entityToSave;
|
||||
}
|
||||
|
||||
private HsOfficeRelationshipEntity persistPartnerRole(final HsOfficePartnerRoleInsertResource resource) {
|
||||
final var entity = new HsOfficeRelationshipEntity();
|
||||
entity.setRelType(HsOfficeRelationshipType.PARTNER);
|
||||
entity.setRelAnchor(ref(HsOfficePersonEntity.class, resource.getRelAnchorUuid()));
|
||||
entity.setRelHolder(ref(HsOfficePersonEntity.class, resource.getRelHolderUuid()));
|
||||
private HsOfficeRelationEntity persistPartnerRel(final HsOfficePartnerRelInsertResource resource) {
|
||||
final var entity = new HsOfficeRelationEntity();
|
||||
entity.setType(HsOfficeRelationType.PARTNER);
|
||||
entity.setAnchor(ref(HsOfficePersonEntity.class, resource.getAnchorUuid()));
|
||||
entity.setHolder(ref(HsOfficePersonEntity.class, resource.getHolderUuid()));
|
||||
entity.setContact(ref(HsOfficeContactEntity.class, resource.getContactUuid()));
|
||||
em.persist(entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private <E extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) {
|
||||
private <E extends RbacObject> E ref(final Class<E> entityClass, final UUID uuid) {
|
||||
try {
|
||||
return em.getReference(entityClass, uuid);
|
||||
} catch (final Throwable exc) {
|
||||
|
@@ -2,14 +2,20 @@ package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -20,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("PartnerDetails")
|
||||
public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
||||
public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
|
||||
HsOfficePartnerDetailsEntity.class,
|
||||
@@ -31,7 +37,6 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
||||
.withProp(HsOfficePartnerDetailsEntity::getBirthday)
|
||||
.withProp(HsOfficePartnerDetailsEntity::getBirthName)
|
||||
.withProp(HsOfficePartnerDetailsEntity::getDateOfDeath)
|
||||
.withSeparator(", ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -55,6 +60,36 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
|
||||
return registrationNumber != null ? registrationNumber
|
||||
: birthName != null ? birthName
|
||||
: birthday != null ? birthday.toString()
|
||||
: dateOfDeath != null ? dateOfDeath.toString() : "<empty details>";
|
||||
: dateOfDeath != null ? dateOfDeath.toString()
|
||||
: "<empty details>";
|
||||
}
|
||||
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class)
|
||||
.withIdentityView(SQL.query("""
|
||||
SELECT partnerDetails.uuid as uuid, partner_iv.idName as idName
|
||||
FROM hs_office_partner_details AS partnerDetails
|
||||
JOIN hs_office_partner partner ON partner.detailsUuid = partnerDetails.uuid
|
||||
JOIN hs_office_partner_iv partner_iv ON partner_iv.uuid = partner.uuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression("uuid"))
|
||||
.withUpdatableColumns(
|
||||
"registrationOffice",
|
||||
"registrationNumber",
|
||||
"birthPlace",
|
||||
"birthName",
|
||||
"birthday",
|
||||
"dateOfDeath")
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
// The grants are defined in HsOfficePartnerEntity.rbac()
|
||||
// because they have to be changed when its partnerRel changes,
|
||||
// not when anything in partner details changes.
|
||||
;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/504-partner/5044-hs-office-partner-details-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,20 +1,40 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.NotFound;
|
||||
import org.hibernate.annotations.NotFoundAction;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.Optional;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Table;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static jakarta.persistence.CascadeType.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.SELECT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -25,12 +45,20 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("Partner")
|
||||
public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
||||
public class HsOfficePartnerEntity implements Stringifyable, RbacObject {
|
||||
|
||||
public static final String PARTNER_NUMBER_TAG = "P-";
|
||||
|
||||
private static Stringify<HsOfficePartnerEntity> stringify = stringify(HsOfficePartnerEntity.class, "partner")
|
||||
.withProp(HsOfficePartnerEntity::getPerson)
|
||||
.withProp(HsOfficePartnerEntity::getContact)
|
||||
.withSeparator(": ")
|
||||
.withIdProp(HsOfficePartnerEntity::toShortString)
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelationEntity::getHolder)
|
||||
.map(HsOfficePersonEntity::toShortString)
|
||||
.orElse(null))
|
||||
.withProp(p -> ofNullable(p.getPartnerRel())
|
||||
.map(HsOfficeRelationEntity::getContact)
|
||||
.map(HsOfficeContactEntity::toShortString)
|
||||
.orElse(null))
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -40,25 +68,19 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
||||
@Column(name = "partnernumber", columnDefinition = "numeric(5) not null")
|
||||
private Integer partnerNumber;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "partnerroleuuid", nullable = false)
|
||||
private HsOfficeRelationshipEntity partnerRole;
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
|
||||
@JoinColumn(name = "partnerreluuid", nullable = false)
|
||||
private HsOfficeRelationEntity partnerRel;
|
||||
|
||||
// TODO: remove, is replaced by partnerRole
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "personuuid", nullable = false)
|
||||
private HsOfficePersonEntity person;
|
||||
|
||||
// TODO: remove, is replaced by partnerRole
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "contactuuid", nullable = false)
|
||||
private HsOfficeContactEntity contact;
|
||||
|
||||
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH }, optional = true)
|
||||
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = true)
|
||||
@JoinColumn(name = "detailsuuid")
|
||||
@NotFound(action = NotFoundAction.IGNORE)
|
||||
private HsOfficePartnerDetailsEntity details;
|
||||
|
||||
public String getTaggedPartnerNumber() {
|
||||
return PARTNER_NUMBER_TAG + partnerNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return stringify.apply(this);
|
||||
@@ -66,6 +88,31 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse("<person=null>");
|
||||
return getTaggedPartnerNumber();
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("partner", HsOfficePartnerEntity.class)
|
||||
.withIdentityView(SQL.projection("'P-' || partnerNumber"))
|
||||
.withUpdatableColumns("partnerRelUuid")
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.importRootEntityAliasProxy("partnerRel", HsOfficeRelationEntity.class,
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("partnerRelUuid"))
|
||||
.createPermission(DELETE).grantedTo("partnerRel", ADMIN)
|
||||
.createPermission(UPDATE).grantedTo("partnerRel", AGENT)
|
||||
.createPermission(SELECT).grantedTo("partnerRel", TENANT)
|
||||
|
||||
.importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class,
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
dependsOnColumn("detailsUuid"))
|
||||
.createPermission("partnerDetails", DELETE).grantedTo("partnerRel", ADMIN)
|
||||
.createPermission("partnerDetails", UPDATE).grantedTo("partnerRel", AGENT)
|
||||
.createPermission("partnerDetails", SELECT).grantedTo("partnerRel", AGENT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/504-partner/5043-hs-office-partner-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,11 @@
|
||||
package net.hostsharing.hsadminng.hs.office.partner;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartnerPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.UUID;
|
||||
|
||||
class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatchResource> {
|
||||
private final EntityManager em;
|
||||
@@ -21,19 +19,15 @@ class HsOfficePartnerEntityPatcher implements EntityPatcher<HsOfficePartnerPatch
|
||||
|
||||
@Override
|
||||
public void apply(final HsOfficePartnerPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "contact");
|
||||
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
||||
});
|
||||
OptionalFromJson.of(resource.getPersonUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "person");
|
||||
entity.setPerson(em.getReference(HsOfficePersonEntity.class, newValue));
|
||||
OptionalFromJson.of(resource.getPartnerRelUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "partnerRel");
|
||||
entity.setPartnerRel(em.getReference(HsOfficeRelationEntity.class, newValue));
|
||||
});
|
||||
|
||||
new HsOfficePartnerDetailsEntityPatcher(em, entity.getDetails()).apply(resource.getDetails());
|
||||
}
|
||||
|
||||
private void verifyNotNull(final UUID newValue, final String propertyName) {
|
||||
private void verifyNotNull(final Object newValue, final String propertyName) {
|
||||
if (newValue == null) {
|
||||
throw new IllegalArgumentException("property '" + propertyName + "' must not be null");
|
||||
}
|
||||
|
@@ -11,10 +11,13 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
|
||||
|
||||
Optional<HsOfficePartnerEntity> findByUuid(UUID id);
|
||||
|
||||
List<HsOfficePartnerEntity> findAll(); // TODO.impl: move to a repo in test sources
|
||||
|
||||
@Query("""
|
||||
SELECT partner FROM HsOfficePartnerEntity partner
|
||||
JOIN HsOfficeContactEntity contact ON contact.uuid = partner.contact.uuid
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = partner.person.uuid
|
||||
JOIN HsOfficeRelationEntity rel ON rel.uuid = partner.partnerRel.uuid
|
||||
JOIN HsOfficeContactEntity contact ON contact.uuid = rel.contact.uuid
|
||||
JOIN HsOfficePersonEntity person ON person.uuid = rel.holder.uuid
|
||||
WHERE :name is null
|
||||
OR partner.details.birthName like concat(cast(:name as text), '%')
|
||||
OR contact.label like concat(cast(:name as text), '%')
|
||||
|
@@ -3,14 +3,22 @@ package net.hostsharing.hsadminng.hs.office.person;
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -22,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
@DisplayName("Person")
|
||||
public class HsOfficePersonEntity implements HasUuid, Stringifyable {
|
||||
public class HsOfficePersonEntity implements RbacObject, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
|
||||
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType)
|
||||
@@ -64,4 +72,28 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable {
|
||||
return personType + " " +
|
||||
(!StringUtils.isEmpty(tradeName) ? tradeName : (StringUtils.isEmpty(salutation) ? "" : salutation + " ") + (familyName + ", " + givenName));
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("person", HsOfficePersonEntity.class)
|
||||
.withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)"))
|
||||
.withUpdatableColumns("personType", "tradeName", "givenName", "familyName")
|
||||
.toRole("global", GUEST).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.permission(DELETE);
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/502-person/5023-hs-office-person-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relationship;
|
||||
package net.hostsharing.hsadminng.hs.office.relation;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRepository;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.api.HsOfficeRelationshipsApi;
|
||||
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.person.HsOfficePersonRepository;
|
||||
import net.hostsharing.hsadminng.mapper.Mapper;
|
||||
@@ -22,7 +22,7 @@ import java.util.function.BiConsumer;
|
||||
|
||||
@RestController
|
||||
|
||||
public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi {
|
||||
public class HsOfficeRelationController implements HsOfficeRelationsApi {
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
@@ -31,10 +31,10 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
||||
private Mapper mapper;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeRelationshipRepository relationshipRepo;
|
||||
private HsOfficeRelationRepository relationRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficePersonRepository relHolderRepo;
|
||||
private HsOfficePersonRepository holderRepo;
|
||||
|
||||
@Autowired
|
||||
private HsOfficeContactRepository contactRepo;
|
||||
@@ -44,79 +44,80 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<HsOfficeRelationshipResource>> listRelationships(
|
||||
public ResponseEntity<List<HsOfficeRelationResource>> listRelations(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID personUuid,
|
||||
final HsOfficeRelationshipTypeResource relationshipType) {
|
||||
final HsOfficeRelationTypeResource relationType) {
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entities = relationshipRepo.findRelationshipRelatedToPersonUuidAndRelationshipType(personUuid,
|
||||
mapper.map(relationshipType, HsOfficeRelationshipType.class));
|
||||
final var entities = relationRepo.findRelationRelatedToPersonUuidAndRelationType(personUuid,
|
||||
mapper.map(relationType, HsOfficeRelationType.class));
|
||||
|
||||
final var resources = mapper.mapList(entities, HsOfficeRelationshipResource.class,
|
||||
RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var resources = mapper.mapList(entities, HsOfficeRelationResource.class,
|
||||
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.ok(resources);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<HsOfficeRelationshipResource> addRelationship(
|
||||
public ResponseEntity<HsOfficeRelationResource> addRelation(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final HsOfficeRelationshipInsertResource body) {
|
||||
final HsOfficeRelationInsertResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var entityToSave = new HsOfficeRelationshipEntity();
|
||||
entityToSave.setRelType(HsOfficeRelationshipType.valueOf(body.getRelType()));
|
||||
entityToSave.setRelAnchor(relHolderRepo.findByUuid(body.getRelAnchorUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find relAnchorUuid " + body.getRelAnchorUuid())
|
||||
final var entityToSave = new HsOfficeRelationEntity();
|
||||
entityToSave.setType(HsOfficeRelationType.valueOf(body.getType()));
|
||||
entityToSave.setMark(body.getMark());
|
||||
entityToSave.setAnchor(holderRepo.findByUuid(body.getAnchorUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find anchorUuid " + body.getAnchorUuid())
|
||||
));
|
||||
entityToSave.setRelHolder(relHolderRepo.findByUuid(body.getRelHolderUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find relHolderUuid " + body.getRelHolderUuid())
|
||||
entityToSave.setHolder(holderRepo.findByUuid(body.getHolderUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find holderUuid " + body.getHolderUuid())
|
||||
));
|
||||
entityToSave.setContact(contactRepo.findByUuid(body.getContactUuid()).orElseThrow(
|
||||
() -> new NoSuchElementException("cannot find contactUuid " + body.getContactUuid())
|
||||
));
|
||||
|
||||
final var saved = relationshipRepo.save(entityToSave);
|
||||
final var saved = relationRepo.save(entityToSave);
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/hs/office/relationships/{id}")
|
||||
.path("/api/hs/office/relations/{id}")
|
||||
.buildAndExpand(saved.getUuid())
|
||||
.toUri();
|
||||
final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class,
|
||||
RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
final var mapped = mapper.map(saved, HsOfficeRelationResource.class,
|
||||
RELATION_ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||
return ResponseEntity.created(uri).body(mapped);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<HsOfficeRelationshipResource> getRelationshipByUuid(
|
||||
public ResponseEntity<HsOfficeRelationResource> getRelationByUuid(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID relationshipUuid) {
|
||||
final UUID relationUuid) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var result = relationshipRepo.findByUuid(relationshipUuid);
|
||||
final var result = relationRepo.findByUuid(relationUuid);
|
||||
if (result.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeRelationshipResource.class, RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
return ResponseEntity.ok(mapper.map(result.get(), HsOfficeRelationResource.class, RELATION_ENTITY_TO_RESOURCE_POSTMAPPER));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<Void> deleteRelationshipByUuid(
|
||||
public ResponseEntity<Void> deleteRelationByUuid(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID relationshipUuid) {
|
||||
final UUID relationUuid) {
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var result = relationshipRepo.deleteByUuid(relationshipUuid);
|
||||
final var result = relationRepo.deleteByUuid(relationUuid);
|
||||
if (result == 0) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
@@ -126,27 +127,27 @@ public class HsOfficeRelationshipController implements HsOfficeRelationshipsApi
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ResponseEntity<HsOfficeRelationshipResource> patchRelationship(
|
||||
public ResponseEntity<HsOfficeRelationResource> patchRelation(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
final UUID relationshipUuid,
|
||||
final HsOfficeRelationshipPatchResource body) {
|
||||
final UUID relationUuid,
|
||||
final HsOfficeRelationPatchResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var current = relationshipRepo.findByUuid(relationshipUuid).orElseThrow();
|
||||
final var current = relationRepo.findByUuid(relationUuid).orElseThrow();
|
||||
|
||||
new HsOfficeRelationshipEntityPatcher(em, current).apply(body);
|
||||
new HsOfficeRelationEntityPatcher(em, current).apply(body);
|
||||
|
||||
final var saved = relationshipRepo.save(current);
|
||||
final var mapped = mapper.map(saved, HsOfficeRelationshipResource.class);
|
||||
final var saved = relationRepo.save(current);
|
||||
final var mapped = mapper.map(saved, HsOfficeRelationResource.class);
|
||||
return ResponseEntity.ok(mapped);
|
||||
}
|
||||
|
||||
|
||||
final BiConsumer<HsOfficeRelationshipEntity, HsOfficeRelationshipResource> RELATIONSHIP_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setRelAnchor(mapper.map(entity.getRelAnchor(), HsOfficePersonResource.class));
|
||||
resource.setRelHolder(mapper.map(entity.getRelHolder(), HsOfficePersonResource.class));
|
||||
final BiConsumer<HsOfficeRelationEntity, HsOfficeRelationResource> RELATION_ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||
resource.setAnchor(mapper.map(entity.getAnchor(), HsOfficePersonResource.class));
|
||||
resource.setHolder(mapper.map(entity.getHolder(), HsOfficePersonResource.class));
|
||||
resource.setContact(mapper.map(entity.getContact(), HsOfficeContactResource.class));
|
||||
};
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relation;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(name = "hs_office_relation_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class HsOfficeRelationEntity implements RbacObject, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeRelationEntity> toString = stringify(HsOfficeRelationEntity.class, "rel")
|
||||
.withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor)
|
||||
.withProp(Fields.type, HsOfficeRelationEntity::getType)
|
||||
.withProp(Fields.mark, HsOfficeRelationEntity::getMark)
|
||||
.withProp(Fields.holder, HsOfficeRelationEntity::getHolder)
|
||||
.withProp(Fields.contact, HsOfficeRelationEntity::getContact);
|
||||
|
||||
private static Stringify<HsOfficeRelationEntity> toShortString = stringify(HsOfficeRelationEntity.class, "rel")
|
||||
.withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor)
|
||||
.withProp(Fields.type, HsOfficeRelationEntity::getType)
|
||||
.withProp(Fields.holder, HsOfficeRelationEntity::getHolder);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "anchoruuid")
|
||||
private HsOfficePersonEntity anchor;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "holderuuid")
|
||||
private HsOfficePersonEntity holder;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "contactuuid")
|
||||
private HsOfficeContactEntity contact;
|
||||
|
||||
@Column(name = "type")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HsOfficeRelationType type;
|
||||
|
||||
@Column(name = "mark")
|
||||
private String mark;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return toShortString.apply(this);
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("relation", HsOfficeRelationEntity.class)
|
||||
.withIdentityView(SQL.projection("""
|
||||
(select idName from hs_office_person_iv p where p.uuid = anchorUuid)
|
||||
|| '-with-' || target.type || '-'
|
||||
|| (select idName from hs_office_person_iv p where p.uuid = holderUuid)
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(SQL.expression(
|
||||
"(select idName from hs_office_person_iv p where p.uuid = target.holderUuid)"))
|
||||
.withUpdatableColumns("contactUuid")
|
||||
.importEntityAlias("anchorPerson", HsOfficePersonEntity.class,
|
||||
dependsOnColumn("anchorUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
.importEntityAlias("holderPerson", HsOfficePersonEntity.class,
|
||||
dependsOnColumn("holderUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
.importEntityAlias("contact", HsOfficeContactEntity.class,
|
||||
dependsOnColumn("contactUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
// TODO: if type=REPRESENTATIIVE
|
||||
// with.incomingSuperRole("holderPerson", ADMIN);
|
||||
with.permission(DELETE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.incomingSuperRole("anchorPerson", ADMIN);
|
||||
// TODO: if type=REPRESENTATIIVE
|
||||
// with.outgoingSuperRole("anchorPerson", OWNER);
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT, (with) -> {
|
||||
with.incomingSuperRole("holderPerson", ADMIN);
|
||||
})
|
||||
.createSubRole(TENANT, (with) -> {
|
||||
with.incomingSuperRole("holderPerson", ADMIN);
|
||||
with.incomingSuperRole("contact", ADMIN);
|
||||
with.outgoingSubRole("anchorPerson", REFERRER);
|
||||
with.outgoingSubRole("holderPerson", REFERRER);
|
||||
with.outgoingSubRole("contact", REFERRER);
|
||||
with.permission(SELECT);
|
||||
})
|
||||
|
||||
.toRole("anchorPerson", ADMIN).grantPermission(INSERT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/503-relation/5033-hs-office-relation-rbac");
|
||||
}
|
||||
}
|
@@ -1,25 +1,25 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relationship;
|
||||
package net.hostsharing.hsadminng.hs.office.relation;
|
||||
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationshipPatchResource;
|
||||
import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficeRelationPatchResource;
|
||||
import net.hostsharing.hsadminng.mapper.EntityPatcher;
|
||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.UUID;
|
||||
|
||||
class HsOfficeRelationshipEntityPatcher implements EntityPatcher<HsOfficeRelationshipPatchResource> {
|
||||
class HsOfficeRelationEntityPatcher implements EntityPatcher<HsOfficeRelationPatchResource> {
|
||||
|
||||
private final EntityManager em;
|
||||
private final HsOfficeRelationshipEntity entity;
|
||||
private final HsOfficeRelationEntity entity;
|
||||
|
||||
HsOfficeRelationshipEntityPatcher(final EntityManager em, final HsOfficeRelationshipEntity entity) {
|
||||
HsOfficeRelationEntityPatcher(final EntityManager em, final HsOfficeRelationEntity entity) {
|
||||
this.em = em;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(final HsOfficeRelationshipPatchResource resource) {
|
||||
public void apply(final HsOfficeRelationPatchResource resource) {
|
||||
OptionalFromJson.of(resource.getContactUuid()).ifPresent(newValue -> {
|
||||
verifyNotNull(newValue, "contact");
|
||||
entity.setContact(em.getReference(HsOfficeContactEntity.class, newValue));
|
@@ -0,0 +1,37 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relation;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficeRelationRepository extends Repository<HsOfficeRelationEntity, UUID> {
|
||||
|
||||
Optional<HsOfficeRelationEntity> findByUuid(UUID id);
|
||||
|
||||
default List<HsOfficeRelationEntity> findRelationRelatedToPersonUuidAndRelationType(@NotNull UUID personUuid, HsOfficeRelationType relationType) {
|
||||
return findRelationRelatedToPersonUuidAndRelationTypeString(personUuid, relationType.toString());
|
||||
}
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM hs_office_relation_rv AS p
|
||||
WHERE p.anchorUuid = :personUuid OR p.holderUuid = :personUuid
|
||||
""", nativeQuery = true)
|
||||
List<HsOfficeRelationEntity> findRelationRelatedToPersonUuid(@NotNull UUID personUuid);
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM hs_office_relation_rv AS p
|
||||
WHERE (:relationType IS NULL OR p.type = cast(:relationType AS HsOfficeRelationType))
|
||||
AND ( p.anchorUuid = :personUuid OR p.holderUuid = :personUuid)
|
||||
""", nativeQuery = true)
|
||||
List<HsOfficeRelationEntity> findRelationRelatedToPersonUuidAndRelationTypeString(@NotNull UUID personUuid, String relationType);
|
||||
|
||||
HsOfficeRelationEntity save(final HsOfficeRelationEntity entity);
|
||||
|
||||
long count();
|
||||
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relationship;
|
||||
package net.hostsharing.hsadminng.hs.office.relation;
|
||||
|
||||
public enum HsOfficeRelationshipType {
|
||||
public enum HsOfficeRelationType {
|
||||
UNKNOWN,
|
||||
PARTNER,
|
||||
EX_PARTNER,
|
||||
REPRESENTATIVE,
|
||||
VIP_CONTACT,
|
||||
ACCOUNTING,
|
||||
DEBITOR,
|
||||
OPERATIONS,
|
||||
SUBSCRIBER
|
||||
}
|
@@ -1,70 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relationship;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.FieldNameConstants;
|
||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@Table(name = "hs_office_relationship_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@FieldNameConstants
|
||||
public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable {
|
||||
|
||||
private static Stringify<HsOfficeRelationshipEntity> toString = stringify(HsOfficeRelationshipEntity.class, "rel")
|
||||
.withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
|
||||
.withProp(Fields.relType, HsOfficeRelationshipEntity::getRelType)
|
||||
.withProp(Fields.relMark, HsOfficeRelationshipEntity::getRelMark)
|
||||
.withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder)
|
||||
.withProp(Fields.contact, HsOfficeRelationshipEntity::getContact);
|
||||
|
||||
private static Stringify<HsOfficeRelationshipEntity> toShortString = stringify(HsOfficeRelationshipEntity.class, "rel")
|
||||
.withProp(Fields.relAnchor, HsOfficeRelationshipEntity::getRelAnchor)
|
||||
.withProp(Fields.relType, HsOfficeRelationshipEntity::getRelType)
|
||||
.withProp(Fields.relHolder, HsOfficeRelationshipEntity::getRelHolder);
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "relanchoruuid")
|
||||
private HsOfficePersonEntity relAnchor;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "relholderuuid")
|
||||
private HsOfficePersonEntity relHolder;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "contactuuid")
|
||||
private HsOfficeContactEntity contact;
|
||||
|
||||
@Column(name = "reltype")
|
||||
@Enumerated(EnumType.STRING)
|
||||
private HsOfficeRelationshipType relType;
|
||||
|
||||
@Column(name = "relmark")
|
||||
private String relMark;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toString.apply(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toShortString() {
|
||||
return toShortString.apply(this);
|
||||
}
|
||||
}
|
@@ -1,37 +0,0 @@
|
||||
package net.hostsharing.hsadminng.hs.office.relationship;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HsOfficeRelationshipRepository extends Repository<HsOfficeRelationshipEntity, UUID> {
|
||||
|
||||
Optional<HsOfficeRelationshipEntity> findByUuid(UUID id);
|
||||
|
||||
default List<HsOfficeRelationshipEntity> findRelationshipRelatedToPersonUuidAndRelationshipType(@NotNull UUID personUuid, HsOfficeRelationshipType relationshipType) {
|
||||
return findRelationshipRelatedToPersonUuidAndRelationshipTypeString(personUuid, relationshipType.toString());
|
||||
}
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM hs_office_relationship_rv AS p
|
||||
WHERE p.relAnchorUuid = :personUuid OR p.relHolderUuid = :personUuid
|
||||
""", nativeQuery = true)
|
||||
List<HsOfficeRelationshipEntity> findRelationshipRelatedToPersonUuid(@NotNull UUID personUuid);
|
||||
|
||||
@Query(value = """
|
||||
SELECT p.* FROM hs_office_relationship_rv AS p
|
||||
WHERE (:relationshipType IS NULL OR p.relType = cast(:relationshipType AS HsOfficeRelationshipType))
|
||||
AND ( p.relAnchorUuid = :personUuid OR p.relHolderUuid = :personUuid)
|
||||
""", nativeQuery = true)
|
||||
List<HsOfficeRelationshipEntity> findRelationshipRelatedToPersonUuidAndRelationshipTypeString(@NotNull UUID personUuid, String relationshipType);
|
||||
|
||||
HsOfficeRelationshipEntity save(final HsOfficeRelationshipEntity entity);
|
||||
|
||||
long count();
|
||||
|
||||
int deleteByUuid(UUID uuid);
|
||||
}
|
@@ -14,7 +14,6 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -57,7 +56,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
||||
public ResponseEntity<HsOfficeSepaMandateResource> addSepaMandate(
|
||||
final String currentUser,
|
||||
final String assumedRoles,
|
||||
@Valid final HsOfficeSepaMandateInsertResource body) {
|
||||
final HsOfficeSepaMandateInsertResource body) {
|
||||
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
@@ -132,6 +131,7 @@ public class HsOfficeSepaMandateController implements HsOfficeSepaMandatesApi {
|
||||
if (entity.getValidity().hasUpperBound()) {
|
||||
resource.setValidTo(entity.getValidity().upper().minusDays(1));
|
||||
}
|
||||
resource.getDebitor().setDebitorNumber(entity.getDebitor().getDebitorNumber());
|
||||
};
|
||||
|
||||
final BiConsumer<HsOfficeSepaMandateInsertResource, HsOfficeSepaMandateEntity> SEPA_MANDATE_RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||
|
@@ -1,21 +1,32 @@
|
||||
package net.hostsharing.hsadminng.hs.office.sepamandate;
|
||||
|
||||
import com.vladmihalcea.hibernate.type.range.PostgreSQLRangeType;
|
||||
import com.vladmihalcea.hibernate.type.range.Range;
|
||||
import io.hypersistence.utils.hibernate.type.range.PostgreSQLRangeType;
|
||||
import io.hypersistence.utils.hibernate.type.range.Range;
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.errors.DisplayName;
|
||||
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
|
||||
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
|
||||
import net.hostsharing.hsadminng.persistence.HasUuid;
|
||||
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.stringify.Stringify;
|
||||
import net.hostsharing.hsadminng.stringify.Stringifyable;
|
||||
import org.hibernate.annotations.Type;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDate;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.mapper.PostgresDateRange.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
|
||||
@Entity
|
||||
@@ -26,14 +37,13 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DisplayName("SEPA-Mandate")
|
||||
public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
|
||||
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject {
|
||||
|
||||
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
|
||||
.withProp(e -> e.getBankAccount().getIban())
|
||||
.withProp(HsOfficeSepaMandateEntity::getReference)
|
||||
.withProp(HsOfficeSepaMandateEntity::getAgreement)
|
||||
.withProp(e -> e.getValidity().asString())
|
||||
.withSeparator(", ")
|
||||
.quotedValues(false);
|
||||
|
||||
@Id
|
||||
@@ -84,4 +94,53 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
|
||||
return reference;
|
||||
}
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class)
|
||||
.withIdentityView(query("""
|
||||
select sm.uuid as uuid, ba.iban || '-' || sm.validity as idName
|
||||
from hs_office_sepamandate sm
|
||||
join hs_office_bankaccount ba on ba.uuid = sm.bankAccountUuid
|
||||
"""))
|
||||
.withRestrictedViewOrderBy(expression("validity"))
|
||||
.withUpdatableColumns("reference", "agreement", "validity")
|
||||
|
||||
.importEntityAlias("debitorRel", HsOfficeRelationEntity.class,
|
||||
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)
|
||||
.importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class,
|
||||
dependsOnColumn("bankAccountUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR);
|
||||
with.incomingSuperRole(GLOBAL, ADMIN);
|
||||
with.permission(DELETE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(AGENT, (with) -> {
|
||||
with.outgoingSubRole("bankAccount", REFERRER);
|
||||
with.outgoingSubRole("debitorRel", AGENT);
|
||||
})
|
||||
.createSubRole(REFERRER, (with) -> {
|
||||
with.incomingSuperRole("bankAccount", ADMIN);
|
||||
with.incomingSuperRole("debitorRel", AGENT);
|
||||
with.outgoingSubRole("debitorRel", TENANT);
|
||||
with.permission(SELECT);
|
||||
})
|
||||
|
||||
.toRole("debitorRel", ADMIN).grantPermission(INSERT);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("5-hs-office/507-sepamandate/5073-hs-office-sepamandate-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.postgresql.util.PGtokenizer;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
@UtilityClass
|
||||
public class PostgresArray {
|
||||
|
||||
/**
|
||||
* Converts a byte[], as returned for a Postgres-array by native queries, to a Java array.
|
||||
*
|
||||
* <p>This example code worked with Hibernate 5 (Spring Boot 3.0.x):
|
||||
* <pre><code>
|
||||
* return (UUID[]) em.createNativeQuery("select currentSubjectsUuids() as uuids", UUID[].class).getSingleResult();
|
||||
* </code></pre>
|
||||
* </p>
|
||||
*
|
||||
* <p>With Hibernate 6 (Spring Boot 3.1.x), this utility method can be used like such:
|
||||
* <pre><code>
|
||||
* final byte[] result = (byte[]) em.createNativeQuery("select * from currentSubjectsUuids() as uuids", UUID[].class)
|
||||
* .getSingleResult();
|
||||
* return fromPostgresArray(result, UUID.class, UUID::fromString);
|
||||
* </code></pre>
|
||||
* </p>
|
||||
*
|
||||
* @param pgArray the byte[] returned by a native query containing as rendered for a Postgres array
|
||||
* @param elementClass the class of a single element of the Java array to be returned
|
||||
* @param itemParser converts a string element to the specified elementClass
|
||||
* @return a Java array containing the data from pgArray
|
||||
* @param <T> type of a single element of the Java array
|
||||
*/
|
||||
public static <T> T[] fromPostgresArray(final byte[] pgArray, final Class<T> elementClass, final Function<String, T> itemParser) {
|
||||
final var pgArrayLiteral = new String(pgArray, StandardCharsets.UTF_8);
|
||||
if (pgArrayLiteral.length() == 2) {
|
||||
return newGenericArray(elementClass, 0);
|
||||
}
|
||||
final PGtokenizer tokenizer = new PGtokenizer(pgArrayLiteral.substring(1, pgArrayLiteral.length()-1), ',');
|
||||
tokenizer.remove("\"", "\"");
|
||||
final T[] array = newGenericArray(elementClass, tokenizer.getSize()); // Create a new array of the specified type and length
|
||||
for ( int n = 0; n < tokenizer.getSize(); ++n ) {
|
||||
final String token = tokenizer.getToken(n);
|
||||
if ( !"NULL".equals(token) ) {
|
||||
array[n] = itemParser.apply(token.trim().replace("\\\"", "\""));
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T[] newGenericArray(final Class<T> elementClass, final int length) {
|
||||
return (T[]) Array.newInstance(elementClass, length);
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
package net.hostsharing.hsadminng.mapper;
|
||||
|
||||
import com.vladmihalcea.hibernate.type.range.Range;
|
||||
import io.hypersistence.utils.hibernate.type.range.Range;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
@@ -1,7 +0,0 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HasUuid {
|
||||
UUID getUuid();
|
||||
}
|
@@ -0,0 +1,260 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.BinaryOperator;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
import static org.apache.commons.lang3.StringUtils.capitalize;
|
||||
import static org.apache.commons.lang3.StringUtils.uncapitalize;
|
||||
|
||||
public class InsertTriggerGenerator {
|
||||
|
||||
private final RbacView rbacDef;
|
||||
private final String liquibaseTagPrefix;
|
||||
|
||||
public InsertTriggerGenerator(final RbacView rbacDef, final String liqibaseTagPrefix) {
|
||||
this.rbacDef = rbacDef;
|
||||
this.liquibaseTagPrefix = liqibaseTagPrefix;
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
generateLiquibaseChangesetHeader(plPgSql);
|
||||
generateGrantInsertRoleToExistingObjects(plPgSql);
|
||||
generateInsertPermissionGrantTrigger(plPgSql);
|
||||
generateInsertCheckTrigger(plPgSql);
|
||||
plPgSql.writeLn("--//");
|
||||
}
|
||||
|
||||
private void generateLiquibaseChangesetHeader(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix));
|
||||
}
|
||||
|
||||
private void generateGrantInsertRoleToExistingObjects(final StringWriter plPgSql) {
|
||||
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
row ${rawSuperTableName};
|
||||
begin
|
||||
call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows');
|
||||
|
||||
FOR row IN SELECT * FROM ${rawSuperTableName}
|
||||
LOOP
|
||||
call grantPermissionToRole(
|
||||
createPermission(row.uuid, 'INSERT', '${rawSubTableName}'),
|
||||
${rawSuperRoleDescriptor});
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
""",
|
||||
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
|
||||
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
|
||||
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, "row"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void generateInsertPermissionGrantTrigger(final StringWriter plPgSql) {
|
||||
getOptionalInsertSuperRole().ifPresent( superRoleDef -> {
|
||||
plPgSql.writeLn("""
|
||||
/**
|
||||
Adds ${rawSubTableName} INSERT permission to specified role of new ${rawSuperTableName} rows.
|
||||
*/
|
||||
create or replace function ${rawSubTableName}_${rawSuperTableName}_insert_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call grantPermissionToRole(
|
||||
createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}'),
|
||||
${rawSuperRoleDescriptor});
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
|
||||
create trigger z_${rawSubTableName}_${rawSuperTableName}_insert_tg
|
||||
after insert on ${rawSuperTableName}
|
||||
for each row
|
||||
execute procedure ${rawSubTableName}_${rawSuperTableName}_insert_tf();
|
||||
""",
|
||||
with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()),
|
||||
with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()),
|
||||
with("rawSuperRoleDescriptor", toRoleDescriptor(superRoleDef, NEW.name()))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private void generateInsertCheckTrigger(final StringWriter plPgSql) {
|
||||
getOptionalInsertGrant().ifPresentOrElse(g -> {
|
||||
if (g.getSuperRoleDef().getEntityAlias().isGlobal()) {
|
||||
switch (g.getSuperRoleDef().getRole()) {
|
||||
case ADMIN -> {
|
||||
generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
|
||||
}
|
||||
case GUEST -> {
|
||||
// no permission check trigger generated, as anybody can insert rows into this table
|
||||
}
|
||||
default -> {
|
||||
throw new IllegalArgumentException(
|
||||
"invalid global role for INSERT permission: " + g.getSuperRoleDef().getRole());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (g.getSuperRoleDef().getEntityAlias().isFetchedByDirectForeignKey()) {
|
||||
generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(plPgSql, g);
|
||||
} else {
|
||||
generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(plPgSql, g);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
System.err.println("WARNING: no explicit INSERT grant for " + rbacDef.getRootEntityAlias().simpleName() + " => implicitly grant INSERT to global:ADMIN");
|
||||
generateInsertPermissionTriggerAllowOnlyGlobalAdmin(plPgSql);
|
||||
});
|
||||
}
|
||||
|
||||
private void generateInsertPermissionTriggerAllowByRoleOfDirectForeignKey(final StringWriter plPgSql, final RbacView.RbacGrantDefinition g) {
|
||||
plPgSql.writeLn("""
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
|
||||
where the check is performed by a direct role.
|
||||
|
||||
A direct role is a role depending on a foreign key directly available in the NEW row.
|
||||
*/
|
||||
create or replace function ${rawSubTable}_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger ${rawSubTable}_insert_permission_check_tg
|
||||
before insert on ${rawSubTable}
|
||||
for each row
|
||||
when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
|
||||
execute procedure ${rawSubTable}_insert_permission_missing_tf();
|
||||
""",
|
||||
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
|
||||
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName()));
|
||||
}
|
||||
|
||||
private void generateInsertPermissionTriggerAllowByRoleOfIndirectForeignKey(
|
||||
final StringWriter plPgSql,
|
||||
final RbacView.RbacGrantDefinition g) {
|
||||
plPgSql.writeLn("""
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
|
||||
where the check is performed by an indirect role.
|
||||
|
||||
An indirect role is a role which depends on an object uuid which is not a direct foreign key
|
||||
of the source entity, but needs to be fetched via joined tables.
|
||||
*/
|
||||
create or replace function ${rawSubTable}_insert_permission_check_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
superRoleObjectUuid uuid;
|
||||
|
||||
begin
|
||||
""",
|
||||
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
|
||||
plPgSql.chopEmptyLines();
|
||||
plPgSql.indented(2, () -> {
|
||||
plPgSql.writeLn(
|
||||
"superRoleObjectUuid := (" + g.getSuperRoleDef().getEntityAlias().fetchSql().sql + ");\n" +
|
||||
"assert superRoleObjectUuid is not null, 'superRoleObjectUuid must not be null';",
|
||||
with("columns", g.getSuperRoleDef().getEntityAlias().aliasName() + ".uuid"),
|
||||
with("ref", NEW.name()));
|
||||
});
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("""
|
||||
if ( not hasInsertPermission(superRoleObjectUuid, 'INSERT', '${rawSubTable}') ) then
|
||||
raise exception
|
||||
'[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end if;
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger ${rawSubTable}_insert_permission_check_tg
|
||||
before insert on ${rawSubTable}
|
||||
for each row
|
||||
execute procedure ${rawSubTable}_insert_permission_check_tf();
|
||||
|
||||
""",
|
||||
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
|
||||
}
|
||||
|
||||
private void generateInsertPermissionTriggerAllowOnlyGlobalAdmin(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable},
|
||||
where only global-admin has that permission.
|
||||
*/
|
||||
create or replace function ${rawSubTable}_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into ${rawSubTable} not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger ${rawSubTable}_insert_permission_check_tg
|
||||
before insert on ${rawSubTable}
|
||||
for each row
|
||||
when ( not isGlobalAdmin() )
|
||||
execute procedure ${rawSubTable}_insert_permission_missing_tf();
|
||||
""",
|
||||
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
|
||||
}
|
||||
|
||||
private Stream<RbacView.RbacGrantDefinition> getInsertGrants() {
|
||||
return rbacDef.getGrantDefs().stream()
|
||||
.filter(g -> g.grantType() == PERM_TO_ROLE)
|
||||
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
|
||||
}
|
||||
|
||||
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
|
||||
return getInsertGrants()
|
||||
.reduce(singleton());
|
||||
}
|
||||
|
||||
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
|
||||
return getInsertGrants()
|
||||
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
|
||||
.reduce(singleton());
|
||||
}
|
||||
|
||||
private static <T> BinaryOperator<T> singleton() {
|
||||
return (x, y) -> {
|
||||
throw new IllegalStateException("only a single INSERT permission grant allowed");
|
||||
};
|
||||
}
|
||||
|
||||
private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
|
||||
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().name());
|
||||
}
|
||||
|
||||
|
||||
private String toRoleDescriptor(final RbacView.RbacRoleDefinition roleDef, final String ref) {
|
||||
final var functionName = toVar(roleDef);
|
||||
if (roleDef.getEntityAlias().isGlobal()) {
|
||||
return functionName + "()";
|
||||
}
|
||||
return functionName + "(" + ref + ")";
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
public enum PostgresTriggerReference {
|
||||
NEW, OLD
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
|
||||
public class RbacIdentityViewGenerator {
|
||||
private final RbacView rbacDef;
|
||||
private final String liquibaseTagPrefix;
|
||||
private final String simpleEntityVarName;
|
||||
private final String rawTableName;
|
||||
|
||||
public RbacIdentityViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||
this.rbacDef = rbacDef;
|
||||
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
|
||||
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix));
|
||||
|
||||
plPgSql.writeLn(
|
||||
switch (rbacDef.getIdentityViewSqlQuery().part) {
|
||||
case SQL_PROJECTION -> """
|
||||
call generateRbacIdentityViewFromProjection('${rawTableName}',
|
||||
$idName$
|
||||
${identityViewSqlPart}
|
||||
$idName$);
|
||||
""";
|
||||
case SQL_QUERY -> """
|
||||
call generateRbacIdentityViewFromQuery('${rawTableName}',
|
||||
$idName$
|
||||
${identityViewSqlPart}
|
||||
$idName$);
|
||||
""";
|
||||
default -> throw new IllegalStateException("illegal SQL part given");
|
||||
},
|
||||
with("identityViewSqlPart", StringWriter.indented(2, rbacDef.getIdentityViewSqlQuery().sql)),
|
||||
with("rawTableName", rawTableName));
|
||||
|
||||
plPgSql.writeLn("--//");
|
||||
}
|
||||
}
|
@@ -0,0 +1,27 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
|
||||
public class RbacObjectGenerator {
|
||||
|
||||
private final String liquibaseTagPrefix;
|
||||
private final String rawTableName;
|
||||
|
||||
public RbacObjectGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('${rawTableName}');
|
||||
--//
|
||||
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||
with("rawTableName", rawTableName));
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.indented;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
|
||||
public class RbacRestrictedViewGenerator {
|
||||
private final RbacView rbacDef;
|
||||
private final String liquibaseTagPrefix;
|
||||
private final String rawTableName;
|
||||
|
||||
public RbacRestrictedViewGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||
this.rbacDef = rbacDef;
|
||||
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('${rawTableName}',
|
||||
$orderBy$
|
||||
${orderBy}
|
||||
$orderBy$,
|
||||
$updates$
|
||||
${updates}
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||
with("orderBy", indented(2, rbacDef.getOrderBySqlExpression().sql)),
|
||||
with("updates", indented(2, rbacDef.getUpdatableColumns().stream()
|
||||
.map(c -> c + " = new." + c)
|
||||
.collect(joining(",\n")))),
|
||||
with("rawTableName", rawTableName));
|
||||
}
|
||||
}
|
@@ -0,0 +1,30 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
|
||||
public class RbacRoleDescriptorsGenerator {
|
||||
|
||||
private final String liquibaseTagPrefix;
|
||||
private final String simpleEntityVarName;
|
||||
private final String rawTableName;
|
||||
|
||||
public RbacRoleDescriptorsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||
this.simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
|
||||
this.rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('${simpleEntityVarName}', '${rawTableName}');
|
||||
--//
|
||||
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||
with("simpleEntityVarName", simpleEntityVarName),
|
||||
with("rawTableName", rawTableName));
|
||||
}
|
||||
}
|
1087
src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
Normal file
1087
src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,162 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.nio.file.*;
|
||||
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*;
|
||||
|
||||
public class RbacViewMermaidFlowchartGenerator {
|
||||
|
||||
public static final String HOSTSHARING_DARK_ORANGE = "#dd4901";
|
||||
public static final String HOSTSHARING_LIGHT_ORANGE = "#feb28c";
|
||||
public static final String HOSTSHARING_DARK_BLUE = "#274d6e";
|
||||
public static final String HOSTSHARING_LIGHT_BLUE = "#99bcdb";
|
||||
private final RbacView rbacDef;
|
||||
private final StringWriter flowchart = new StringWriter();
|
||||
|
||||
public RbacViewMermaidFlowchartGenerator(final RbacView rbacDef) {
|
||||
this.rbacDef = rbacDef;
|
||||
flowchart.writeLn("""
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
""");
|
||||
renderEntitySubgraphs();
|
||||
renderGrants();
|
||||
}
|
||||
private void renderEntitySubgraphs() {
|
||||
rbacDef.getEntityAliases().values().stream()
|
||||
.filter(entityAlias -> !rbacDef.isEntityAliasProxy(entityAlias))
|
||||
.filter(entityAlias -> !entityAlias.isPlaceholder())
|
||||
.forEach(this::renderEntitySubgraph);
|
||||
}
|
||||
|
||||
private void renderEntitySubgraph(final RbacView.EntityAlias entity) {
|
||||
final var color = rbacDef.isRootEntityAlias(entity) ? HOSTSHARING_DARK_ORANGE
|
||||
: entity.isSubEntity() ? HOSTSHARING_LIGHT_ORANGE
|
||||
: HOSTSHARING_LIGHT_BLUE;
|
||||
flowchart.writeLn("""
|
||||
subgraph %{aliasName}["`**%{aliasName}**`"]
|
||||
direction TB
|
||||
style %{aliasName} fill:%{fillColor},stroke:%{strokeColor},stroke-width:8px
|
||||
"""
|
||||
.replace("%{aliasName}", entity.aliasName())
|
||||
.replace("%{fillColor}", color )
|
||||
.replace("%{strokeColor}", HOSTSHARING_DARK_BLUE ));
|
||||
|
||||
flowchart.indented( () -> {
|
||||
rbacDef.getEntityAliases().values().stream()
|
||||
.filter(e -> e.aliasName().startsWith(entity.aliasName() + ":"))
|
||||
.forEach(this::renderEntitySubgraph);
|
||||
|
||||
wrapOutputInSubgraph(entity.aliasName() + ":roles", color,
|
||||
rbacDef.getRoleDefs().stream()
|
||||
.filter(r -> r.getEntityAlias() == entity)
|
||||
.map(this::roleDef)
|
||||
.collect(joining("\n")));
|
||||
|
||||
wrapOutputInSubgraph(entity.aliasName() + ":permissions", color,
|
||||
rbacDef.getPermDefs().stream()
|
||||
.filter(p -> p.getEntityAlias() == entity)
|
||||
.map(this::permDef)
|
||||
.collect(joining("\n")));
|
||||
|
||||
if (rbacDef.isRootEntityAlias(entity) && rbacDef.getRootEntityAliasProxy() != null ) {
|
||||
renderEntitySubgraph(rbacDef.getRootEntityAliasProxy());
|
||||
}
|
||||
|
||||
});
|
||||
flowchart.chopEmptyLines();
|
||||
flowchart.writeLn("end");
|
||||
flowchart.writeLn();
|
||||
}
|
||||
|
||||
private void wrapOutputInSubgraph(final String name, final String color, final String content) {
|
||||
if (!StringUtils.isEmpty(content)) {
|
||||
flowchart.ensureSingleEmptyLine();
|
||||
flowchart.writeLn("subgraph " + name + "[ ]\n");
|
||||
flowchart.indented(() -> {
|
||||
flowchart.writeLn("style %{aliasName} fill:%{fillColor},stroke:white"
|
||||
.replace("%{aliasName}", name)
|
||||
.replace("%{fillColor}", color));
|
||||
flowchart.writeLn();
|
||||
flowchart.writeLn(content);
|
||||
});
|
||||
flowchart.chopEmptyLines();
|
||||
flowchart.writeLn("end");
|
||||
flowchart.writeLn();
|
||||
}
|
||||
}
|
||||
|
||||
private void renderGrants() {
|
||||
renderGrants(ROLE_TO_USER, "%% granting roles to users");
|
||||
renderGrants(ROLE_TO_ROLE, "%% granting roles to roles");
|
||||
renderGrants(PERM_TO_ROLE, "%% granting permissions to roles");
|
||||
}
|
||||
|
||||
private void renderGrants(final RbacView.RbacGrantDefinition.GrantType grantType, final String comment) {
|
||||
final var grantsOfRequestedType = rbacDef.getGrantDefs().stream()
|
||||
.filter(g -> g.grantType() == grantType)
|
||||
.toList();
|
||||
if ( !grantsOfRequestedType.isEmpty()) {
|
||||
flowchart.ensureSingleEmptyLine();
|
||||
flowchart.writeLn(comment);
|
||||
grantsOfRequestedType.forEach(g -> flowchart.writeLn(grantDef(g)));
|
||||
}
|
||||
}
|
||||
|
||||
private String grantDef(final RbacView.RbacGrantDefinition grant) {
|
||||
final var arrow = (grant.isToCreate() ? " ==>" : " -.->")
|
||||
+ (grant.isAssumed() ? " " : "|XX| ");
|
||||
return switch (grant.grantType()) {
|
||||
case ROLE_TO_USER ->
|
||||
// TODO: other user types not implemented yet
|
||||
"user:creator" + arrow + roleId(grant.getSubRoleDef());
|
||||
case ROLE_TO_ROLE ->
|
||||
roleId(grant.getSuperRoleDef()) + arrow + roleId(grant.getSubRoleDef());
|
||||
case PERM_TO_ROLE -> roleId(grant.getSuperRoleDef()) + arrow + permId(grant.getPermDef());
|
||||
};
|
||||
}
|
||||
|
||||
private String permDef(final RbacView.RbacPermissionDefinition perm) {
|
||||
return permId(perm) + "{{" + perm.getEntityAlias().aliasName() + perm.getPermission() + "}}";
|
||||
}
|
||||
|
||||
private static String permId(final RbacView.RbacPermissionDefinition permDef) {
|
||||
return "perm:" + permDef.getEntityAlias().aliasName() + permDef.getPermission();
|
||||
}
|
||||
|
||||
private String roleDef(final RbacView.RbacRoleDefinition roleDef) {
|
||||
return roleId(roleDef) + "[[" + roleDef.getEntityAlias().aliasName() + roleDef.getRole() + "]]";
|
||||
}
|
||||
|
||||
private static String roleId(final RbacView.RbacRoleDefinition r) {
|
||||
return "role:" + r.getEntityAlias().aliasName() + r.getRole();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return flowchart.toString();
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void generateToMarkdownFile(final Path path) {
|
||||
Files.writeString(
|
||||
path,
|
||||
"""
|
||||
### rbac %{entityAlias}
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
|
||||
|
||||
```mermaid
|
||||
%{flowchart}
|
||||
```
|
||||
"""
|
||||
.replace("%{entityAlias}", rbacDef.getRootEntityAlias().aliasName())
|
||||
.replace("%{flowchart}", flowchart.toString()),
|
||||
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
System.out.println("Markdown-File: " + path.toAbsolutePath());
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
|
||||
public class RbacViewPostgresGenerator {
|
||||
|
||||
private final RbacView rbacDef;
|
||||
private final String liqibaseTagPrefix;
|
||||
private final StringWriter plPgSql = new StringWriter();
|
||||
|
||||
public RbacViewPostgresGenerator(final RbacView forRbacDef) {
|
||||
rbacDef = forRbacDef;
|
||||
liqibaseTagPrefix = rbacDef.getRootEntityAlias().getRawTableName().replace("_", "-");
|
||||
plPgSql.writeLn("""
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by ${generator}, do not amend manually.
|
||||
""",
|
||||
with("generator", getClass().getSimpleName()),
|
||||
with("ref", NEW.name()));
|
||||
|
||||
new RbacObjectGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
new RbacRoleDescriptorsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
new RolesGrantsAndPermissionsGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
new InsertTriggerGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
new RbacIdentityViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
new RbacRestrictedViewGenerator(rbacDef, liqibaseTagPrefix).generateTo(plPgSql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return plPgSql.toString()
|
||||
.replace("\n\n\n", "\n\n")
|
||||
.replace("-- ====", "\n-- ====")
|
||||
.replace("\n\n--//", "\n--//");
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public void generateToChangeLog(final Path outputPath) {
|
||||
Files.writeString(
|
||||
outputPath,
|
||||
toString(),
|
||||
StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
System.out.println(outputPath.toAbsolutePath());
|
||||
}
|
||||
}
|
@@ -0,0 +1,573 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacPermissionDefinition;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.OLD;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with;
|
||||
import static org.apache.commons.lang3.StringUtils.capitalize;
|
||||
import static org.apache.commons.lang3.StringUtils.uncapitalize;
|
||||
|
||||
class RolesGrantsAndPermissionsGenerator {
|
||||
|
||||
private final RbacView rbacDef;
|
||||
private final Set<RbacView.RbacGrantDefinition> rbacGrants = new HashSet<>();
|
||||
private final String liquibaseTagPrefix;
|
||||
private final String simpleEntityName;
|
||||
private final String simpleEntityVarName;
|
||||
private final String rawTableName;
|
||||
|
||||
RolesGrantsAndPermissionsGenerator(final RbacView rbacDef, final String liquibaseTagPrefix) {
|
||||
this.rbacDef = rbacDef;
|
||||
this.rbacGrants.addAll(rbacDef.getGrantDefs().stream()
|
||||
.filter(RbacView.RbacGrantDefinition::isToCreate)
|
||||
.collect(toSet()));
|
||||
this.liquibaseTagPrefix = liquibaseTagPrefix;
|
||||
|
||||
simpleEntityVarName = rbacDef.getRootEntityAlias().simpleName();
|
||||
simpleEntityName = capitalize(simpleEntityVarName);
|
||||
rawTableName = rbacDef.getRootEntityAlias().getRawTableName();
|
||||
}
|
||||
|
||||
void generateTo(final StringWriter plPgSql) {
|
||||
generateInsertTrigger(plPgSql);
|
||||
if (hasAnyUpdatableEntityAliases()) {
|
||||
generateUpdateTrigger(plPgSql);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateHeader(final StringWriter plPgSql, final String triggerType) {
|
||||
plPgSql.writeLn("""
|
||||
-- ============================================================================
|
||||
--changeset ${liquibaseTagPrefix}-rbac-${triggerType}-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
""",
|
||||
with("liquibaseTagPrefix", liquibaseTagPrefix),
|
||||
with("triggerType", triggerType));
|
||||
}
|
||||
|
||||
private void generateInsertTriggerFunction(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace procedure buildRbacSystemFor${simpleEntityName}(
|
||||
NEW ${rawTableName}
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
"""
|
||||
.replace("${simpleEntityName}", simpleEntityName)
|
||||
.replace("${rawTableName}", rawTableName));
|
||||
|
||||
plPgSql.chopEmptyLines();
|
||||
plPgSql.indented(() -> {
|
||||
referencedEntityAliases()
|
||||
.forEach((ea) -> plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";"));
|
||||
});
|
||||
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("begin");
|
||||
plPgSql.indented(() -> {
|
||||
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
|
||||
plPgSql.writeLn();
|
||||
generateCreateRolesAndGrantsAfterInsert(plPgSql);
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
|
||||
});
|
||||
plPgSql.writeLn("end; $$;");
|
||||
plPgSql.writeLn();
|
||||
}
|
||||
|
||||
|
||||
private void generateSimplifiedUpdateTriggerFunction(final StringWriter plPgSql) {
|
||||
|
||||
final var updateConditions = updatableEntityAliases()
|
||||
.map(RbacView.EntityAlias::dependsOnColumName)
|
||||
.distinct()
|
||||
.map(columnName -> "NEW." + columnName + " is distinct from OLD." + columnName)
|
||||
.collect(joining( "\n or "));
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
*/
|
||||
|
||||
create or replace procedure updateRbacRulesFor${simpleEntityName}(
|
||||
OLD ${rawTableName},
|
||||
NEW ${rawTableName}
|
||||
)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
|
||||
if ${updateConditions} then
|
||||
delete from rbacgrants g where g.grantedbytriggerof = OLD.uuid;
|
||||
call buildRbacSystemFor${simpleEntityName}(NEW);
|
||||
end if;
|
||||
end; $$;
|
||||
""",
|
||||
with("simpleEntityName", simpleEntityName),
|
||||
with("rawTableName", rawTableName),
|
||||
with("updateConditions", updateConditions));
|
||||
}
|
||||
|
||||
private void generateUpdateTriggerFunction(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
*/
|
||||
|
||||
create or replace procedure updateRbacRulesFor${simpleEntityName}(
|
||||
OLD ${rawTableName},
|
||||
NEW ${rawTableName}
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
"""
|
||||
.replace("${simpleEntityName}", simpleEntityName)
|
||||
.replace("${rawTableName}", rawTableName));
|
||||
|
||||
plPgSql.chopEmptyLines();
|
||||
plPgSql.indented(() -> {
|
||||
referencedEntityAliases()
|
||||
.forEach((ea) -> {
|
||||
plPgSql.writeLn(entityRefVar(OLD, ea) + " " + ea.getRawTableName() + ";");
|
||||
plPgSql.writeLn(entityRefVar(NEW, ea) + " " + ea.getRawTableName() + ";");
|
||||
});
|
||||
});
|
||||
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("begin");
|
||||
plPgSql.indented(() -> {
|
||||
plPgSql.writeLn("call enterTriggerForObjectUuid(NEW.uuid);");
|
||||
plPgSql.writeLn();
|
||||
generateUpdateRolesAndGrantsAfterUpdate(plPgSql);
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);");
|
||||
});
|
||||
plPgSql.writeLn("end; $$;");
|
||||
plPgSql.writeLn();
|
||||
}
|
||||
|
||||
private boolean hasAnyUpdatableEntityAliases() {
|
||||
return updatableEntityAliases().anyMatch(e -> true);
|
||||
}
|
||||
|
||||
private boolean hasAnyUpdatableAndNullableEntityAliases() {
|
||||
return updatableEntityAliases()
|
||||
.filter(ea -> ea.nullable() == RbacView.Nullable.NULLABLE)
|
||||
.anyMatch(e -> true);
|
||||
}
|
||||
|
||||
private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) {
|
||||
referencedEntityAliases()
|
||||
.forEach((ea) -> {
|
||||
generateFetchedVars(plPgSql, ea, NEW);
|
||||
plPgSql.writeLn();
|
||||
});
|
||||
|
||||
createRolesWithGrantsSql(plPgSql, OWNER);
|
||||
createRolesWithGrantsSql(plPgSql, ADMIN);
|
||||
createRolesWithGrantsSql(plPgSql, AGENT);
|
||||
createRolesWithGrantsSql(plPgSql, TENANT);
|
||||
createRolesWithGrantsSql(plPgSql, REFERRER);
|
||||
|
||||
generateGrants(plPgSql, ROLE_TO_USER);
|
||||
generateGrants(plPgSql, ROLE_TO_ROLE);
|
||||
generateGrants(plPgSql, PERM_TO_ROLE);
|
||||
}
|
||||
|
||||
private Stream<RbacView.EntityAlias> referencedEntityAliases() {
|
||||
return rbacDef.getEntityAliases().values().stream()
|
||||
.filter(ea -> !rbacDef.isRootEntityAlias(ea))
|
||||
.filter(ea -> ea.dependsOnColum() != null)
|
||||
.filter(ea -> ea.entityClass() != null)
|
||||
.filter(ea -> ea.fetchSql() != null);
|
||||
}
|
||||
|
||||
private Stream<RbacView.EntityAlias> updatableEntityAliases() {
|
||||
return referencedEntityAliases()
|
||||
.filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column));
|
||||
}
|
||||
|
||||
private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) {
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
|
||||
referencedEntityAliases()
|
||||
.forEach((ea) -> {
|
||||
generateFetchedVars(plPgSql, ea, OLD);
|
||||
generateFetchedVars(plPgSql, ea, NEW);
|
||||
plPgSql.writeLn();
|
||||
});
|
||||
|
||||
updatableEntityAliases()
|
||||
.map(RbacView.EntityAlias::dependsOnColum)
|
||||
.map(c -> c.column)
|
||||
.sorted()
|
||||
.distinct()
|
||||
.forEach(columnName -> {
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("if NEW." + columnName + " <> OLD." + columnName + " then");
|
||||
plPgSql.indented(() -> {
|
||||
updateGrantsDependingOn(plPgSql, columnName);
|
||||
});
|
||||
plPgSql.writeLn("end if;");
|
||||
});
|
||||
}
|
||||
|
||||
private void generateFetchedVars(
|
||||
final StringWriter plPgSql,
|
||||
final RbacView.EntityAlias ea,
|
||||
final PostgresTriggerReference old) {
|
||||
plPgSql.writeLn(
|
||||
ea.fetchSql().sql + " INTO " + entityRefVar(old, ea) + ";",
|
||||
with("columns", ea.aliasName() + ".*"),
|
||||
with("ref", old.name()));
|
||||
if (ea.nullable() == RbacView.Nullable.NOT_NULL) {
|
||||
plPgSql.writeLn(
|
||||
"assert ${entityRefVar}.uuid is not null, format('${entityRefVar} must not be null for ${REF}.${dependsOnColumn} = %s', ${REF}.${dependsOnColumn});",
|
||||
with("entityRefVar", entityRefVar(old, ea)),
|
||||
with("dependsOnColumn", ea.dependsOnColumName()),
|
||||
with("ref", old.name()));
|
||||
plPgSql.writeLn();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGrantsDependingOn(final StringWriter plPgSql, final String columnName) {
|
||||
rbacDef.getGrantDefs().stream()
|
||||
.filter(RbacView.RbacGrantDefinition::isToCreate)
|
||||
.filter(g -> g.dependsOnColumn(columnName))
|
||||
.filter(g -> !isInsertPermissionGrant(g))
|
||||
.forEach(g -> {
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
plPgSql.writeLn(generateRevoke(g));
|
||||
plPgSql.writeLn(generateGrant(g));
|
||||
plPgSql.writeLn();
|
||||
});
|
||||
}
|
||||
|
||||
private static Boolean isInsertPermissionGrant(final RbacView.RbacGrantDefinition g) {
|
||||
final var isInsertPermissionGrant = ofNullable(g.getPermDef()).map(RbacPermissionDefinition::getPermission).map(p -> p == INSERT).orElse(false);
|
||||
return isInsertPermissionGrant;
|
||||
}
|
||||
|
||||
private void generateGrants(final StringWriter plPgSql, final RbacView.RbacGrantDefinition.GrantType grantType) {
|
||||
plPgSql.ensureSingleEmptyLine();
|
||||
rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == grantType)
|
||||
.map(this::generateGrant)
|
||||
.sorted()
|
||||
.forEach(text -> plPgSql.writeLn(text));
|
||||
}
|
||||
|
||||
private String generateRevoke(RbacView.RbacGrantDefinition grantDef) {
|
||||
return switch (grantDef.grantType()) {
|
||||
case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant");
|
||||
case ROLE_TO_ROLE -> "call revokeRoleFromRole(${subRoleRef}, ${superRoleRef});"
|
||||
.replace("${subRoleRef}", roleRef(OLD, grantDef.getSubRoleDef()))
|
||||
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
|
||||
case PERM_TO_ROLE -> "call revokePermissionFromRole(${permRef}, ${superRoleRef});"
|
||||
.replace("${permRef}", getPerm(OLD, grantDef.getPermDef()))
|
||||
.replace("${superRoleRef}", roleRef(OLD, grantDef.getSuperRoleDef()));
|
||||
};
|
||||
}
|
||||
|
||||
private String generateGrant(RbacView.RbacGrantDefinition grantDef) {
|
||||
return switch (grantDef.grantType()) {
|
||||
case ROLE_TO_USER -> throw new IllegalArgumentException("unexpected grant");
|
||||
case ROLE_TO_ROLE -> "call grantRoleToRole(${subRoleRef}, ${superRoleRef}${assumed});"
|
||||
.replace("${assumed}", grantDef.isAssumed() ? "" : ", unassumed()")
|
||||
.replace("${subRoleRef}", roleRef(NEW, grantDef.getSubRoleDef()))
|
||||
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
|
||||
case PERM_TO_ROLE ->
|
||||
grantDef.getPermDef().getPermission() == INSERT ? ""
|
||||
: "call grantPermissionToRole(${permRef}, ${superRoleRef});"
|
||||
.replace("${permRef}", createPerm(NEW, grantDef.getPermDef()))
|
||||
.replace("${superRoleRef}", roleRef(NEW, grantDef.getSuperRoleDef()));
|
||||
};
|
||||
}
|
||||
|
||||
private String findPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
|
||||
return permRef("findPermissionId", ref, permDef);
|
||||
}
|
||||
|
||||
private String getPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
|
||||
return permRef("getPermissionId", ref, permDef);
|
||||
}
|
||||
|
||||
private String createPerm(final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
|
||||
return permRef("createPermission", ref, permDef);
|
||||
}
|
||||
|
||||
private String permRef(final String functionName, final PostgresTriggerReference ref, final RbacPermissionDefinition permDef) {
|
||||
return "${prefix}(${entityRef}.uuid, '${perm}')"
|
||||
.replace("${prefix}", functionName)
|
||||
.replace("${entityRef}", rbacDef.isRootEntityAlias(permDef.entityAlias)
|
||||
? ref.name()
|
||||
: refVarName(ref, permDef.entityAlias))
|
||||
.replace("${perm}", permDef.permission.name());
|
||||
}
|
||||
|
||||
private String refVarName(final PostgresTriggerReference ref, final RbacView.EntityAlias entityAlias) {
|
||||
return ref.name().toLowerCase() + capitalize(entityAlias.aliasName());
|
||||
}
|
||||
|
||||
private String roleRef(final PostgresTriggerReference rootRefVar, final RbacView.RbacRoleDefinition roleDef) {
|
||||
if (roleDef == null) {
|
||||
System.out.println("null");
|
||||
}
|
||||
if (roleDef.getEntityAlias().isGlobal()) {
|
||||
return "globalAdmin()";
|
||||
}
|
||||
final String entityRefVar = entityRefVar(rootRefVar, roleDef.getEntityAlias());
|
||||
return roleDef.getEntityAlias().simpleName() + capitalize(roleDef.getRole().name())
|
||||
+ "(" + entityRefVar + ")";
|
||||
}
|
||||
|
||||
private String entityRefVar(
|
||||
final PostgresTriggerReference rootRefVar,
|
||||
final RbacView.EntityAlias entityAlias) {
|
||||
return rbacDef.isRootEntityAlias(entityAlias)
|
||||
? rootRefVar.name()
|
||||
: rootRefVar.name().toLowerCase() + capitalize(entityAlias.aliasName());
|
||||
}
|
||||
|
||||
private void createRolesWithGrantsSql(final StringWriter plPgSql, final RbacView.Role role) {
|
||||
|
||||
final var isToCreate = rbacDef.getRoleDefs().stream()
|
||||
.filter(roleDef -> rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) && roleDef.getRole() == role)
|
||||
.findFirst().map(RbacView.RbacRoleDefinition::isToCreate).orElse(false);
|
||||
if (!isToCreate) {
|
||||
return;
|
||||
}
|
||||
|
||||
plPgSql.writeLn();
|
||||
plPgSql.writeLn("perform createRoleWithGrants(");
|
||||
plPgSql.indented(() -> {
|
||||
plPgSql.writeLn("${simpleVarName)${roleSuffix}(NEW),"
|
||||
.replace("${simpleVarName)", simpleEntityVarName)
|
||||
.replace("${roleSuffix}", capitalize(role.name())));
|
||||
|
||||
generatePermissionsForRole(plPgSql, role);
|
||||
|
||||
generateIncomingSuperRolesForRole(plPgSql, role);
|
||||
|
||||
generateOutgoingSubRolesForRole(plPgSql, role);
|
||||
|
||||
generateUserGrantsForRole(plPgSql, role);
|
||||
|
||||
plPgSql.chopTail(",\n");
|
||||
plPgSql.writeLn();
|
||||
});
|
||||
|
||||
plPgSql.writeLn(");");
|
||||
}
|
||||
|
||||
private void generateUserGrantsForRole(final StringWriter plPgSql, final RbacView.Role role) {
|
||||
final var grantsToUsers = findGrantsToUserForRole(rbacDef.getRootEntityAlias(), role);
|
||||
if (!grantsToUsers.isEmpty()) {
|
||||
final var arrayElements = grantsToUsers.stream()
|
||||
.map(RbacView.RbacGrantDefinition::getUserDef)
|
||||
.map(this::toPlPgSqlReference)
|
||||
.toList();
|
||||
plPgSql.indented(() ->
|
||||
plPgSql.writeLn("userUuids => array[" + joinArrayElements(arrayElements, 2) + "],\n"));
|
||||
rbacGrants.removeAll(grantsToUsers);
|
||||
}
|
||||
}
|
||||
|
||||
private void generatePermissionsForRole(final StringWriter plPgSql, final RbacView.Role role) {
|
||||
final var permissionGrantsForRole = findPermissionsGrantsForRole(rbacDef.getRootEntityAlias(), role);
|
||||
if (!permissionGrantsForRole.isEmpty()) {
|
||||
final var arrayElements = permissionGrantsForRole.stream()
|
||||
.map(RbacView.RbacGrantDefinition::getPermDef)
|
||||
.map(RbacPermissionDefinition::getPermission)
|
||||
.map(RbacView.Permission::name)
|
||||
.map(p -> "'" + p + "'")
|
||||
.sorted()
|
||||
.toList();
|
||||
plPgSql.indented(() ->
|
||||
plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n"));
|
||||
rbacGrants.removeAll(permissionGrantsForRole);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateIncomingSuperRolesForRole(final StringWriter plPgSql, final RbacView.Role role) {
|
||||
final var incomingGrants = findIncomingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
|
||||
if (!incomingGrants.isEmpty()) {
|
||||
final var arrayElements = incomingGrants.stream()
|
||||
.map(g -> toPlPgSqlReference(NEW, g.getSuperRoleDef(), g.isAssumed()))
|
||||
.sorted().toList();
|
||||
plPgSql.indented(() ->
|
||||
plPgSql.writeLn("incomingSuperRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
|
||||
rbacGrants.removeAll(incomingGrants);
|
||||
}
|
||||
}
|
||||
|
||||
private void generateOutgoingSubRolesForRole(final StringWriter plPgSql, final RbacView.Role role) {
|
||||
final var outgoingGrants = findOutgoingSuperRolesForRole(rbacDef.getRootEntityAlias(), role);
|
||||
if (!outgoingGrants.isEmpty()) {
|
||||
final var arrayElements = outgoingGrants.stream()
|
||||
.map(g -> toPlPgSqlReference(NEW, g.getSubRoleDef(), g.isAssumed()))
|
||||
.sorted().toList();
|
||||
plPgSql.indented(() ->
|
||||
plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n"));
|
||||
rbacGrants.removeAll(outgoingGrants);
|
||||
}
|
||||
}
|
||||
|
||||
private String joinArrayElements(final List<String> arrayElements, final int singleLineLimit) {
|
||||
return arrayElements.size() <= singleLineLimit
|
||||
? String.join(", ", arrayElements)
|
||||
: arrayElements.stream().collect(joining(",\n\t", "\n\t", ""));
|
||||
}
|
||||
|
||||
private Set<RbacView.RbacGrantDefinition> findPermissionsGrantsForRole(
|
||||
final RbacView.EntityAlias entityAlias,
|
||||
final RbacView.Role role) {
|
||||
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||
return rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == PERM_TO_ROLE && g.getSuperRoleDef() == roleDef)
|
||||
.collect(toSet());
|
||||
}
|
||||
|
||||
private Set<RbacView.RbacGrantDefinition> findGrantsToUserForRole(
|
||||
final RbacView.EntityAlias entityAlias,
|
||||
final RbacView.Role role) {
|
||||
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||
return rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == ROLE_TO_USER && g.getSubRoleDef() == roleDef)
|
||||
.collect(toSet());
|
||||
}
|
||||
|
||||
private Set<RbacView.RbacGrantDefinition> findIncomingSuperRolesForRole(
|
||||
final RbacView.EntityAlias entityAlias,
|
||||
final RbacView.Role role) {
|
||||
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||
return rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSubRoleDef() == roleDef)
|
||||
.collect(toSet());
|
||||
}
|
||||
|
||||
private Set<RbacView.RbacGrantDefinition> findOutgoingSuperRolesForRole(
|
||||
final RbacView.EntityAlias entityAlias,
|
||||
final RbacView.Role role) {
|
||||
final var roleDef = rbacDef.findRbacRole(entityAlias, role);
|
||||
return rbacGrants.stream()
|
||||
.filter(g -> g.grantType() == ROLE_TO_ROLE && g.getSuperRoleDef() == roleDef)
|
||||
.filter(g -> g.getSubRoleDef().getEntityAlias() != entityAlias)
|
||||
.collect(toSet());
|
||||
}
|
||||
|
||||
private void generateInsertTrigger(final StringWriter plPgSql) {
|
||||
|
||||
generateHeader(plPgSql, "insert");
|
||||
generateInsertTriggerFunction(plPgSql);
|
||||
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new ${rawTableName} row.
|
||||
*/
|
||||
|
||||
create or replace function insertTriggerFor${simpleEntityName}_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call buildRbacSystemFor${simpleEntityName}(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger insertTriggerFor${simpleEntityName}_tg
|
||||
after insert on ${rawTableName}
|
||||
for each row
|
||||
execute procedure insertTriggerFor${simpleEntityName}_tf();
|
||||
"""
|
||||
.replace("${simpleEntityName}", simpleEntityName)
|
||||
.replace("${rawTableName}", rawTableName)
|
||||
);
|
||||
|
||||
generateFooter(plPgSql);
|
||||
}
|
||||
|
||||
private void generateUpdateTrigger(final StringWriter plPgSql) {
|
||||
|
||||
generateHeader(plPgSql, "update");
|
||||
if ( hasAnyUpdatableAndNullableEntityAliases() ) {
|
||||
generateSimplifiedUpdateTriggerFunction(plPgSql);
|
||||
} else {
|
||||
generateUpdateTriggerFunction(plPgSql);
|
||||
}
|
||||
|
||||
plPgSql.writeLn("""
|
||||
/*
|
||||
AFTER INSERT TRIGGER to re-wire the grant structure for a new ${rawTableName} row.
|
||||
*/
|
||||
|
||||
create or replace function updateTriggerFor${simpleEntityName}_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call updateRbacRulesFor${simpleEntityName}(OLD, NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger updateTriggerFor${simpleEntityName}_tg
|
||||
after update on ${rawTableName}
|
||||
for each row
|
||||
execute procedure updateTriggerFor${simpleEntityName}_tf();
|
||||
"""
|
||||
.replace("${simpleEntityName}", simpleEntityName)
|
||||
.replace("${rawTableName}", rawTableName)
|
||||
);
|
||||
|
||||
generateFooter(plPgSql);
|
||||
}
|
||||
|
||||
private static void generateFooter(final StringWriter plPgSql) {
|
||||
plPgSql.writeLn("--//");
|
||||
plPgSql.writeLn();
|
||||
}
|
||||
|
||||
private String toPlPgSqlReference(final RbacView.RbacUserReference userRef) {
|
||||
return switch (userRef.role) {
|
||||
case CREATOR -> "currentUserUuid()";
|
||||
default -> throw new IllegalArgumentException("unknown user role: " + userRef);
|
||||
};
|
||||
}
|
||||
|
||||
private String toPlPgSqlReference(
|
||||
final PostgresTriggerReference triggerRef,
|
||||
final RbacView.RbacRoleDefinition roleDef,
|
||||
final boolean assumed) {
|
||||
final var assumedArg = assumed ? "" : ", unassumed()";
|
||||
return toRoleRef(roleDef) +
|
||||
(roleDef.getEntityAlias().isGlobal() ? ( assumed ? "()" : "(unassumed())")
|
||||
: rbacDef.isRootEntityAlias(roleDef.getEntityAlias()) ? ("(" + triggerRef.name() + ")")
|
||||
: "(" + 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(
|
||||
final PostgresTriggerReference triggerRef,
|
||||
final RbacView.EntityAlias entityAlias) {
|
||||
return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName());
|
||||
}
|
||||
}
|
@@ -0,0 +1,121 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
public class StringWriter {
|
||||
|
||||
private final StringBuilder string = new StringBuilder();
|
||||
private int indentLevel = 0;
|
||||
|
||||
static VarDef with(final String var, final String name) {
|
||||
return new VarDef(var, name);
|
||||
}
|
||||
|
||||
void writeLn(final String text) {
|
||||
string.append( indented(text));
|
||||
writeLn();
|
||||
}
|
||||
|
||||
void writeLn(final String text, final VarDef... varDefs) {
|
||||
string.append( indented( new VarReplacer(varDefs).apply(text) ));
|
||||
writeLn();
|
||||
}
|
||||
|
||||
void writeLn() {
|
||||
string.append( "\n");
|
||||
}
|
||||
|
||||
void indent() {
|
||||
++indentLevel;
|
||||
}
|
||||
|
||||
void unindent() {
|
||||
--indentLevel;
|
||||
}
|
||||
|
||||
void indent(int levels) {
|
||||
indentLevel += levels;
|
||||
}
|
||||
|
||||
void unindent(int levels) {
|
||||
indentLevel -= levels;
|
||||
}
|
||||
|
||||
void indented(final Runnable indented) {
|
||||
indent();
|
||||
indented.run();
|
||||
unindent();
|
||||
}
|
||||
|
||||
void indented(int levels, final Runnable indented) {
|
||||
indent(levels);
|
||||
indented.run();
|
||||
unindent(levels);
|
||||
}
|
||||
|
||||
boolean chopTail(final String tail) {
|
||||
if (string.toString().endsWith(tail)) {
|
||||
string.setLength(string.length() - tail.length());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void chopEmptyLines() {
|
||||
while (string.toString().endsWith("\n\n")) {
|
||||
string.setLength(string.length() - 1);
|
||||
};
|
||||
}
|
||||
|
||||
void ensureSingleEmptyLine() {
|
||||
chopEmptyLines();
|
||||
writeLn();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return string.toString();
|
||||
}
|
||||
|
||||
public static String indented(final int indentLevel, final String text) {
|
||||
final var indentation = StringUtils.repeat(" ", indentLevel);
|
||||
final var indented = stream(text.split("\n"))
|
||||
.map(line -> line.trim().isBlank() ? "" : indentation + line)
|
||||
.collect(joining("\n"));
|
||||
return indented;
|
||||
}
|
||||
|
||||
private String indented(final String text) {
|
||||
if ( indentLevel == 0) {
|
||||
return text;
|
||||
}
|
||||
return indented(indentLevel, text);
|
||||
}
|
||||
|
||||
record VarDef(String name, String value){}
|
||||
|
||||
private static final class VarReplacer {
|
||||
|
||||
private final VarDef[] varDefs;
|
||||
private String text;
|
||||
|
||||
private VarReplacer(VarDef[] varDefs) {
|
||||
this.varDefs = varDefs;
|
||||
}
|
||||
|
||||
String apply(final String textToAppend) {
|
||||
text = textToAppend;
|
||||
stream(varDefs).forEach(varDef -> {
|
||||
final var pattern = Pattern.compile("\\$\\{" + varDef.name() + "}", Pattern.CASE_INSENSITIVE);
|
||||
final var matcher = pattern.matcher(text);
|
||||
text = matcher.replaceAll(varDef.value());
|
||||
});
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
// TODO: The whole code in this package is more like a quick hack to solve an urgent problem.
|
||||
// It should be re-written in PostgreSQL pl/pgsql,
|
||||
// so that no Java is needed to use this RBAC system in it's full extend.
|
@@ -1,13 +1,13 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||
|
||||
import lombok.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.data.annotation.Immutable;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -20,7 +20,7 @@ import java.util.UUID;
|
||||
@Immutable
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RawRbacGrantEntity {
|
||||
public class RawRbacGrantEntity implements Comparable {
|
||||
|
||||
@Id
|
||||
private UUID uuid;
|
||||
@@ -64,4 +64,9 @@ public class RawRbacGrantEntity {
|
||||
// TODO: remove .distinct() once partner.person + partner.contact are removed
|
||||
return roles.stream().map(RawRbacGrantEntity::toDisplay).sorted().distinct().toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(final Object o) {
|
||||
return uuid.compareTo(((RawRbacGrantEntity)o).uuid);
|
||||
}
|
||||
}
|
@@ -8,4 +8,8 @@ import java.util.UUID;
|
||||
public interface RawRbacGrantRepository extends Repository<RawRbacGrantEntity, UUID> {
|
||||
|
||||
List<RawRbacGrantEntity> findAll();
|
||||
|
||||
List<RawRbacGrantEntity> findByAscendingUuid(UUID ascendingUuid);
|
||||
|
||||
List<RawRbacGrantEntity> findByDescendantUuid(UUID refUuid);
|
||||
}
|
@@ -94,4 +94,17 @@ public class RbacGrantController implements RbacGrantsApi {
|
||||
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
// TODO: implement an endpoint to create a Mermaid flowchart with all grants of a given user
|
||||
// @GetMapping(
|
||||
// path = "/api/rbac/users/{userUuid}/grants",
|
||||
// produces = {"text/vnd.mermaid"})
|
||||
// @Transactional(readOnly = true)
|
||||
// public ResponseEntity<String> allGrantsOfUserAsMermaid(
|
||||
// @RequestHeader(name = "current-user") String currentUser,
|
||||
// @RequestHeader(name = "assumed-roles", required = false) String assumedRoles) {
|
||||
// final var graph = RbacGrantsDiagramService.allGrantsToUser(currentUser);
|
||||
// return ResponseEntity.ok(graph);
|
||||
// }
|
||||
|
||||
}
|
||||
|
@@ -59,9 +59,9 @@ public class RbacGrantEntity {
|
||||
}
|
||||
|
||||
public String toDisplay() {
|
||||
return "{ grant role " + grantedRoleIdName +
|
||||
" to user " + granteeUserName +
|
||||
" by role " + grantedByRoleIdName +
|
||||
return "{ grant role:" + grantedRoleIdName +
|
||||
" to user:" + granteeUserName +
|
||||
" by role:" + grantedByRoleIdName +
|
||||
(assumed ? " and assume" : "") +
|
||||
" }";
|
||||
}
|
||||
|
@@ -0,0 +1,228 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacgrant;
|
||||
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include.*;
|
||||
|
||||
// TODO: cleanup - this code was 'hacked' to quickly fix a specific problem, needs refactoring
|
||||
@Service
|
||||
public class RbacGrantsDiagramService {
|
||||
|
||||
private static final int GRANT_LIMIT = 500;
|
||||
|
||||
public static void writeToFile(final String title, final String graph, final String fileName) {
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
|
||||
writer.write("""
|
||||
### all grants to %s
|
||||
|
||||
```mermaid
|
||||
%s
|
||||
```
|
||||
""".formatted(title, graph));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Include {
|
||||
DETAILS,
|
||||
USERS,
|
||||
PERMISSIONS,
|
||||
NOT_ASSUMED,
|
||||
TEST_ENTITIES,
|
||||
NON_TEST_ENTITIES;
|
||||
|
||||
public static final EnumSet<Include> ALL = EnumSet.allOf(Include.class);
|
||||
public static final EnumSet<Include> ALL_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, TEST_ENTITIES, PERMISSIONS);
|
||||
public static final EnumSet<Include> ALL_NON_TEST_ENTITY_RELATED = EnumSet.of(USERS, DETAILS, NOT_ASSUMED, NON_TEST_ENTITIES, PERMISSIONS);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private Context context;
|
||||
|
||||
@Autowired
|
||||
private RawRbacGrantRepository rawGrantRepo;
|
||||
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
public String allGrantsToCurrentUser(final EnumSet<Include> includes) {
|
||||
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
||||
for ( UUID subjectUuid: context.currentSubjectsUuids() ) {
|
||||
traverseGrantsTo(graph, subjectUuid, includes);
|
||||
}
|
||||
return toMermaidFlowchart(graph, includes);
|
||||
}
|
||||
|
||||
private void traverseGrantsTo(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> includes) {
|
||||
final var grants = rawGrantRepo.findByAscendingUuid(refUuid);
|
||||
grants.forEach(g -> {
|
||||
if (!includes.contains(PERMISSIONS) && g.getDescendantIdName().startsWith("perm:")) {
|
||||
return;
|
||||
}
|
||||
if ( !g.getDescendantIdName().startsWith("role:global")) {
|
||||
if (!includes.contains(TEST_ENTITIES) && g.getDescendantIdName().contains(":test_")) {
|
||||
return;
|
||||
}
|
||||
if (!includes.contains(NON_TEST_ENTITIES) && !g.getDescendantIdName().contains(":test_")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
graph.add(g);
|
||||
if (includes.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||
traverseGrantsTo(graph, g.getDescendantUuid(), includes);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public String allGrantsFrom(final UUID targetObject, final String op, final EnumSet<Include> includes) {
|
||||
final var refUuid = (UUID) em.createNativeQuery("SELECT uuid FROM rbacpermission WHERE objectuuid=:targetObject AND op=:op")
|
||||
.setParameter("targetObject", targetObject)
|
||||
.setParameter("op", op)
|
||||
.getSingleResult();
|
||||
final var graph = new LimitedHashSet<RawRbacGrantEntity>();
|
||||
traverseGrantsFrom(graph, refUuid, includes);
|
||||
return toMermaidFlowchart(graph, includes);
|
||||
}
|
||||
|
||||
private void traverseGrantsFrom(final Set<RawRbacGrantEntity> graph, final UUID refUuid, final EnumSet<Include> option) {
|
||||
final var grants = rawGrantRepo.findByDescendantUuid(refUuid);
|
||||
grants.forEach(g -> {
|
||||
if (!option.contains(USERS) && g.getAscendantIdName().startsWith("user:")) {
|
||||
return;
|
||||
}
|
||||
graph.add(g);
|
||||
if (option.contains(NOT_ASSUMED) || g.isAssumed()) {
|
||||
traverseGrantsFrom(graph, g.getAscendingUuid(), option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String toMermaidFlowchart(final HashSet<RawRbacGrantEntity> graph, final EnumSet<Include> includes) {
|
||||
final var entities =
|
||||
includes.contains(DETAILS)
|
||||
? graph.stream()
|
||||
.flatMap(g -> Stream.of(
|
||||
new Node(g.getAscendantIdName(), g.getAscendingUuid()),
|
||||
new Node(g.getDescendantIdName(), g.getDescendantUuid()))
|
||||
)
|
||||
.collect(groupingBy(RbacGrantsDiagramService::renderEntityIdName))
|
||||
.entrySet().stream()
|
||||
.map(entity -> "subgraph " + cleanId(entity.getKey()) + renderSubgraph(entity.getKey()) + "\n\n "
|
||||
+ entity.getValue().stream()
|
||||
.map(n -> renderNode(n.idName(), n.uuid()).replace("\n", "\n "))
|
||||
.sorted()
|
||||
.distinct()
|
||||
.collect(joining("\n\n ")))
|
||||
.collect(joining("\n\nend\n\n"))
|
||||
+ "\n\nend\n\n"
|
||||
: "";
|
||||
|
||||
final var grants = graph.stream()
|
||||
.map(g -> cleanId(g.getAscendantIdName())
|
||||
+ " -->" + (g.isAssumed() ? " " : "|XX| ")
|
||||
+ cleanId(g.getDescendantIdName()))
|
||||
.sorted()
|
||||
.collect(joining("\n"));
|
||||
|
||||
final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n";
|
||||
return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "")
|
||||
+ (graph.size() >= GRANT_LIMIT ? "%% too many grants, graph is cropped\n" : "")
|
||||
+ "flowchart TB\n\n"
|
||||
+ entities
|
||||
+ grants;
|
||||
}
|
||||
|
||||
private String renderSubgraph(final String entityId) {
|
||||
// this does not work according to Mermaid bug https://github.com/mermaid-js/mermaid/issues/3806
|
||||
// if (entityId.contains("#")) {
|
||||
// final var parts = entityId.split("#");
|
||||
// final var table = parts[0];
|
||||
// final var entity = parts[1];
|
||||
// if (table.equals("entity")) {
|
||||
// return "[" + entity "]";
|
||||
// }
|
||||
// return "[" + table + "\n" + entity + "]";
|
||||
// }
|
||||
return "[" + cleanId(entityId) + "]";
|
||||
}
|
||||
|
||||
private static String renderEntityIdName(final Node node) {
|
||||
final var refType = refType(node.idName());
|
||||
if (refType.equals("user")) {
|
||||
return "users";
|
||||
}
|
||||
if (refType.equals("perm")) {
|
||||
return node.idName().split(" ", 4)[3];
|
||||
}
|
||||
if (refType.equals("role")) {
|
||||
final var withoutRolePrefix = node.idName().substring("role:".length());
|
||||
return withoutRolePrefix.substring(0, withoutRolePrefix.lastIndexOf(':'));
|
||||
}
|
||||
throw new IllegalArgumentException("unknown refType '" + refType + "' in '" + node.idName() + "'");
|
||||
}
|
||||
|
||||
private String renderNode(final String idName, final UUID uuid) {
|
||||
return cleanId(idName) + renderNodeContent(idName, uuid);
|
||||
}
|
||||
|
||||
private String renderNodeContent(final String idName, final UUID uuid) {
|
||||
final var refType = refType(idName);
|
||||
|
||||
if (refType.equals("user")) {
|
||||
final var displayName = idName.substring(refType.length()+1);
|
||||
return "(" + displayName + "\nref:" + uuid + ")";
|
||||
}
|
||||
if (refType.equals("role")) {
|
||||
final var roleType = idName.substring(idName.lastIndexOf(':') + 1);
|
||||
return "[" + roleType + "\nref:" + uuid + "]";
|
||||
}
|
||||
if (refType.equals("perm")) {
|
||||
final var roleType = idName.split(":")[1];
|
||||
return "{{" + roleType + "\nref:" + uuid + "}}";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static String refType(final String idName) {
|
||||
return idName.split(":", 2)[0];
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String cleanId(final String idName) {
|
||||
return idName.replaceAll("@.*", "")
|
||||
.replace("[", "").replace("]", "").replace("(", "").replace(")", "").replace(",", "");
|
||||
}
|
||||
|
||||
|
||||
class LimitedHashSet<T> extends HashSet<T> {
|
||||
|
||||
@Override
|
||||
public boolean add(final T t) {
|
||||
if (size() < GRANT_LIMIT ) {
|
||||
return super.add(t);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Node(String idName, UUID uuid) {
|
||||
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacobject;
|
||||
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface RbacObject {
|
||||
UUID getUuid();
|
||||
}
|
@@ -34,6 +34,6 @@ public class RbacRoleEntity {
|
||||
@Enumerated(EnumType.STRING)
|
||||
private RbacRoleType roleType;
|
||||
|
||||
@Formula("objectTable||'#'||objectIdName||'.'||roleType")
|
||||
@Formula("objectTable||'#'||objectIdName||':'||roleType")
|
||||
private String roleName;
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacrole;
|
||||
|
||||
public enum RbacRoleType {
|
||||
owner, admin, agent, tenant, guest
|
||||
OWNER, ADMIN, AGENT, TENANT, GUEST, REFERRER
|
||||
}
|
||||
|
@@ -8,8 +8,8 @@ public interface RbacUserPermission {
|
||||
String getRoleName();
|
||||
UUID getPermissionUuid();
|
||||
String getOp();
|
||||
String getOpTableName();
|
||||
String getObjectTable();
|
||||
String getObjectIdName();
|
||||
UUID getObjectUuid();
|
||||
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ public final class Stringify<B> {
|
||||
|
||||
private final Class<B> clazz;
|
||||
private final String name;
|
||||
private Function<B, ?> idProp;
|
||||
private final List<Property<B>> props = new ArrayList<>();
|
||||
private String separator = ", ";
|
||||
private Boolean quotedValues = null;
|
||||
@@ -42,6 +43,11 @@ public final class Stringify<B> {
|
||||
}
|
||||
}
|
||||
|
||||
public Stringify<B> withIdProp(final Function<B, ?> getter) {
|
||||
idProp = getter;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Stringify<B> withProp(final String propName, final Function<B, ?> getter) {
|
||||
props.add(new Property<>(propName, getter));
|
||||
return this;
|
||||
@@ -64,7 +70,9 @@ public final class Stringify<B> {
|
||||
})
|
||||
.map(propVal -> propName(propVal, "=") + optionallyQuoted(propVal))
|
||||
.collect(Collectors.joining(separator));
|
||||
return name + "(" + propValues + ")";
|
||||
return idProp != null
|
||||
? name + "(" + idProp.apply(object) + ": " + propValues + ")"
|
||||
: name + "(" + propValues + ")";
|
||||
}
|
||||
|
||||
public Stringify<B> withSeparator(final String separator) {
|
||||
|
@@ -10,6 +10,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.PersistenceContext;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@@ -24,6 +26,9 @@ public class TestCustomerController implements TestCustomersApi {
|
||||
@Autowired
|
||||
private TestCustomerRepository testCustomerRepository;
|
||||
|
||||
@PersistenceContext
|
||||
EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
public ResponseEntity<List<TestCustomerResource>> listCustomers(
|
||||
@@ -48,7 +53,6 @@ public class TestCustomerController implements TestCustomersApi {
|
||||
context.define(currentUser, assumedRoles);
|
||||
|
||||
final var saved = testCustomerRepository.save(mapper.map(customer, TestCustomerEntity.class));
|
||||
|
||||
final var uri =
|
||||
MvcUriComponentsBuilder.fromController(getClass())
|
||||
.path("/api/test/customers/{id}")
|
||||
|
@@ -4,17 +4,27 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.GLOBAL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "test_customer_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestCustomerEntity {
|
||||
public class TestCustomerEntity implements RbacObject {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@@ -25,4 +35,28 @@ public class TestCustomerEntity {
|
||||
|
||||
@Column(name = "adminusername")
|
||||
private String adminUserName;
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("customer", TestCustomerEntity.class)
|
||||
.withIdentityView(SQL.projection("prefix"))
|
||||
.withRestrictedViewOrderBy(SQL.expression("reference"))
|
||||
.withUpdatableColumns("reference", "prefix", "adminUserName")
|
||||
.toRole("global", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.owningUser(CREATOR).unassumed();
|
||||
with.incomingSuperRole(GLOBAL, ADMIN).unassumed();
|
||||
with.permission(DELETE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(TENANT, (with) -> {
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("2-test/201-test-customer/2013-test-customer-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,72 @@
|
||||
package net.hostsharing.hsadminng.test.dom;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.directlyFetchedByDependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "test_domain_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestDomainEntity implements RbacObject {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID uuid;
|
||||
|
||||
@Version
|
||||
private int version;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "packageuuid")
|
||||
private TestPackageEntity pac;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("domain", TestDomainEntity.class)
|
||||
.withIdentityView(SQL.projection("name"))
|
||||
.withUpdatableColumns("version", "packageUuid", "description")
|
||||
|
||||
.importEntityAlias("package", TestPackageEntity.class,
|
||||
dependsOnColumn("packageUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
.toRole("package", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.incomingSuperRole("package", ADMIN);
|
||||
with.outgoingSubRole("package", TENANT);
|
||||
with.permission(DELETE);
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(ADMIN, (with) -> {
|
||||
with.outgoingSubRole("package", TENANT);
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("2-test/203-test-domain/2033-test-domain-rbac");
|
||||
}
|
||||
}
|
@@ -4,18 +4,29 @@ import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
|
||||
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
|
||||
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Nullable.NOT_NULL;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.*;
|
||||
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "test_package_rv")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TestPackageEntity {
|
||||
public class TestPackageEntity implements RbacObject {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@@ -31,4 +42,32 @@ public class TestPackageEntity {
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
|
||||
public static RbacView rbac() {
|
||||
return rbacViewFor("package", TestPackageEntity.class)
|
||||
.withIdentityView(SQL.projection("name"))
|
||||
.withUpdatableColumns("version", "customerUuid", "description")
|
||||
|
||||
.importEntityAlias("customer", TestCustomerEntity.class,
|
||||
dependsOnColumn("customerUuid"),
|
||||
directlyFetchedByDependsOnColumn(),
|
||||
NOT_NULL)
|
||||
.toRole("customer", ADMIN).grantPermission(INSERT)
|
||||
|
||||
.createRole(OWNER, (with) -> {
|
||||
with.incomingSuperRole("customer", ADMIN);
|
||||
with.permission(DELETE);
|
||||
with.permission(UPDATE);
|
||||
})
|
||||
.createSubRole(ADMIN)
|
||||
.createSubRole(TENANT, (with) -> {
|
||||
with.outgoingSubRole("customer", TENANT);
|
||||
with.permission(SELECT);
|
||||
});
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
rbac().generateWithBaseFileName("2-test/202-test-package/2023-test-package-rbac");
|
||||
}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ map:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
/api/hs/office/persons/{personUUID}:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
/api/hs/office/relationships/{relationshipUUID}:
|
||||
/api/hs/office/relations/{relationUUID}:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
/api/hs/office/bankaccounts/{bankAccountUUID}:
|
||||
null: org.openapitools.jackson.nullable.JsonNullable
|
||||
|
@@ -9,6 +9,8 @@ components:
|
||||
uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
debitorRel:
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
debitorNumber:
|
||||
type: integer
|
||||
format: int32
|
||||
@@ -21,8 +23,6 @@ components:
|
||||
maximum: 99
|
||||
partner:
|
||||
$ref: './hs-office-partner-schemas.yaml#/components/schemas/HsOfficePartner'
|
||||
billingContact:
|
||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
||||
billable:
|
||||
type: boolean
|
||||
vatId:
|
||||
@@ -43,7 +43,7 @@ components:
|
||||
HsOfficeDebitorPatch:
|
||||
type: object
|
||||
properties:
|
||||
billingContactUuid:
|
||||
debitorRelUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
@@ -75,14 +75,11 @@ components:
|
||||
HsOfficeDebitorInsert:
|
||||
type: object
|
||||
properties:
|
||||
partnerUuid:
|
||||
debitorRel:
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
||||
debitorRelUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
billingContactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
debitorNumberSuffix:
|
||||
type: integer
|
||||
format: int8
|
||||
@@ -105,9 +102,7 @@ components:
|
||||
defaultPrefix:
|
||||
type: string
|
||||
pattern: '^[a-z]{3}$'
|
||||
|
||||
required:
|
||||
- partnerUuid
|
||||
- billingContactUuid
|
||||
- debitorNumberSuffix
|
||||
- defaultPrefix
|
||||
- billable
|
||||
|
@@ -46,10 +46,6 @@ components:
|
||||
HsOfficeMembershipPatch:
|
||||
type: object
|
||||
properties:
|
||||
mainDebitorUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
validTo:
|
||||
type: string
|
||||
format: date
|
||||
@@ -69,10 +65,6 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
mainDebitorUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: false
|
||||
memberNumberSuffix:
|
||||
type: string
|
||||
minLength: 2
|
||||
@@ -95,7 +87,6 @@ components:
|
||||
required:
|
||||
- partnerUuid
|
||||
- memberNumberSuffix
|
||||
- mainDebitorUuid
|
||||
- validFrom
|
||||
- membershipFeeBillable
|
||||
additionalProperties: false
|
||||
|
@@ -14,10 +14,8 @@ components:
|
||||
format: int8
|
||||
minimum: 10000
|
||||
maximum: 99999
|
||||
person:
|
||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||
contact:
|
||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
||||
partnerRel:
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
details:
|
||||
$ref: '#/components/schemas/HsOfficePartnerDetails'
|
||||
|
||||
@@ -52,11 +50,7 @@ components:
|
||||
HsOfficePartnerPatch:
|
||||
type: object
|
||||
properties:
|
||||
personUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
contactUuid:
|
||||
partnerRelUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
@@ -96,38 +90,31 @@ components:
|
||||
format: int8
|
||||
minimum: 10000
|
||||
maximum: 99999
|
||||
partnerRole:
|
||||
$ref: '#/components/schemas/HsOfficePartnerRoleInsert'
|
||||
personUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
contactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
partnerRel:
|
||||
$ref: '#/components/schemas/HsOfficePartnerRelInsert'
|
||||
details:
|
||||
$ref: '#/components/schemas/HsOfficePartnerDetailsInsert'
|
||||
required:
|
||||
- partnerNumber
|
||||
- personUuid
|
||||
- contactUuid
|
||||
- partnerRel
|
||||
- details
|
||||
|
||||
HsOfficePartnerRoleInsert:
|
||||
HsOfficePartnerRelInsert:
|
||||
type: object
|
||||
nullable: false
|
||||
properties:
|
||||
relAnchorUuid:
|
||||
anchorUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
relHolderUuid:
|
||||
holderUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
contactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- relAnchorUuid
|
||||
- relHolderUuid
|
||||
- anchorUuid
|
||||
- holderUuid
|
||||
- relContactUuid
|
||||
|
||||
HsOfficePartnerDetailsInsert:
|
||||
|
@@ -3,37 +3,37 @@ components:
|
||||
|
||||
schemas:
|
||||
|
||||
HsOfficeRelationshipType:
|
||||
HsOfficeRelationType:
|
||||
type: string
|
||||
enum:
|
||||
- UNKNOWN
|
||||
- PARTNER
|
||||
- EX_PARTNER
|
||||
- REPRESENTATIVE,
|
||||
- DEBITOR
|
||||
- REPRESENTATIVE
|
||||
- VIP_CONTACT
|
||||
- ACCOUNTING,
|
||||
- OPERATIONS
|
||||
- SUBSCRIBER
|
||||
|
||||
HsOfficeRelationship:
|
||||
HsOfficeRelation:
|
||||
type: object
|
||||
properties:
|
||||
uuid:
|
||||
type: string
|
||||
format: uuid
|
||||
relAnchor:
|
||||
anchor:
|
||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||
relHolder:
|
||||
holder:
|
||||
$ref: './hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||
relType:
|
||||
type:
|
||||
type: string
|
||||
relMark:
|
||||
mark:
|
||||
type: string
|
||||
nullable: true
|
||||
contact:
|
||||
$ref: './hs-office-contact-schemas.yaml#/components/schemas/HsOfficeContact'
|
||||
|
||||
HsOfficeRelationshipPatch:
|
||||
HsOfficeRelationPatch:
|
||||
type: object
|
||||
properties:
|
||||
contactUuid:
|
||||
@@ -41,25 +41,26 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
|
||||
HsOfficeRelationshipInsert:
|
||||
HsOfficeRelationInsert:
|
||||
type: object
|
||||
properties:
|
||||
relAnchorUuid:
|
||||
anchorUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
relHolderUuid:
|
||||
holderUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
relType:
|
||||
type:
|
||||
type: string
|
||||
nullable: true
|
||||
relMark:
|
||||
mark:
|
||||
type: string
|
||||
nullable: true
|
||||
contactUuid:
|
||||
type: string
|
||||
format: uuid
|
||||
required:
|
||||
- relAnchorUuid
|
||||
- relHolderUuid
|
||||
- relType
|
||||
- relContactUuid
|
||||
- anchorUuid
|
||||
- holderUuid
|
||||
- type
|
||||
- contactUuid
|
@@ -1,25 +1,25 @@
|
||||
get:
|
||||
tags:
|
||||
- hs-office-relationships
|
||||
description: 'Fetch a single person relationship by its uuid, if visible for the current subject.'
|
||||
operationId: getRelationshipByUuid
|
||||
- hs-office-relations
|
||||
description: 'Fetch a single person relation by its uuid, if visible for the current subject.'
|
||||
operationId: getRelationByUuid
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: relationshipUUID
|
||||
- name: relationUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of the relationship to fetch.
|
||||
description: UUID of the relation to fetch.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
@@ -28,13 +28,13 @@ get:
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- hs-office-relationships
|
||||
description: 'Updates a single person relationship by its uuid, if permitted for the current subject.'
|
||||
operationId: patchRelationship
|
||||
- hs-office-relations
|
||||
description: 'Updates a single person relation by its uuid, if permitted for the current subject.'
|
||||
operationId: patchRelation
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: relationshipUUID
|
||||
- name: relationUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
@@ -44,14 +44,14 @@ patch:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipPatch'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationPatch'
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
@@ -59,19 +59,19 @@ patch:
|
||||
|
||||
delete:
|
||||
tags:
|
||||
- hs-office-relationships
|
||||
description: 'Delete a single person relationship by its uuid, if permitted for the current subject.'
|
||||
operationId: deleteRelationshipByUuid
|
||||
- hs-office-relations
|
||||
description: 'Delete a single person relation by its uuid, if permitted for the current subject.'
|
||||
operationId: deleteRelationByUuid
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: relationshipUUID
|
||||
- name: relationUUID
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: UUID of the relationship to delete.
|
||||
description: UUID of the relation to delete.
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
@@ -1,9 +1,9 @@
|
||||
get:
|
||||
summary: Returns a list of (optionally filtered) person relationships for a given person.
|
||||
description: Returns the list of (optionally filtered) person relationships of a given person and which are visible to the current user or any of it's assumed roles.
|
||||
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 user or any of it's assumed roles.
|
||||
tags:
|
||||
- hs-office-relationships
|
||||
operationId: listRelationships
|
||||
- hs-office-relations
|
||||
operationId: listRelations
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
@@ -13,13 +13,13 @@ get:
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Prefix of name properties from relHolder or contact to filter the results.
|
||||
- name: relationshipType
|
||||
description: Prefix of name properties from holder or contact to filter the results.
|
||||
- name: relationType
|
||||
in: query
|
||||
required: false
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipType'
|
||||
description: Prefix of name properties from relHolder or contact to filter the results.
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationType'
|
||||
description: Prefix of name properties from holder or contact to filter the results.
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
@@ -28,17 +28,17 @@ get:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
||||
$ref: './error-responses.yaml#/components/responses/Forbidden'
|
||||
|
||||
post:
|
||||
summary: Adds a new person relationship.
|
||||
summary: Adds a new person relation.
|
||||
tags:
|
||||
- hs-office-relationships
|
||||
operationId: addRelationship
|
||||
- hs-office-relations
|
||||
operationId: addRelation
|
||||
parameters:
|
||||
- $ref: './auth.yaml#/components/parameters/currentUser'
|
||||
- $ref: './auth.yaml#/components/parameters/assumedRoles'
|
||||
@@ -46,7 +46,7 @@ post:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationshipInsert'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelationInsert'
|
||||
required: true
|
||||
responses:
|
||||
"201":
|
||||
@@ -54,7 +54,7 @@ post:
|
||||
content:
|
||||
'application/json':
|
||||
schema:
|
||||
$ref: './hs-office-relationship-schemas.yaml#/components/schemas/HsOfficeRelationship'
|
||||
$ref: './hs-office-relation-schemas.yaml#/components/schemas/HsOfficeRelation'
|
||||
"401":
|
||||
$ref: './error-responses.yaml#/components/responses/Unauthorized'
|
||||
"403":
|
@@ -35,13 +35,13 @@ paths:
|
||||
$ref: "./hs-office-persons-with-uuid.yaml"
|
||||
|
||||
|
||||
# Relationships
|
||||
# Relations
|
||||
|
||||
/api/hs/office/relationships:
|
||||
$ref: "./hs-office-relationships.yaml"
|
||||
/api/hs/office/relations:
|
||||
$ref: "./hs-office-relations.yaml"
|
||||
|
||||
/api/hs/office/relationships/{relationshipUUID}:
|
||||
$ref: "./hs-office-relationships-with-uuid.yaml"
|
||||
/api/hs/office/relations/{relationUUID}:
|
||||
$ref: "./hs-office-relations-with-uuid.yaml"
|
||||
|
||||
|
||||
# BankAccounts
|
||||
|
@@ -19,8 +19,11 @@ components:
|
||||
roleType:
|
||||
type: string
|
||||
enum:
|
||||
- owner
|
||||
- admin
|
||||
- tenant
|
||||
- OWNER
|
||||
- ADMIN
|
||||
- AGENT
|
||||
- TENANT
|
||||
- REFERRER
|
||||
- GUEST
|
||||
roleName:
|
||||
type: string
|
||||
|
@@ -3,7 +3,7 @@
|
||||
|
||||
-- ============================================================================
|
||||
-- NUMERIC-HASH-FUNCTIONS
|
||||
--changeset hash:1 endDelimiter:--//
|
||||
--changeset numeric-hash-functions:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create function bigIntHash(text) returns bigint as $$
|
@@ -0,0 +1,20 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLE-COLUMNS-FUNCTION
|
||||
--changeset table-columns-function:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace function columnsNames( tableName text )
|
||||
returns text
|
||||
stable
|
||||
language 'plpgsql' as $$
|
||||
declare columns text[];
|
||||
begin
|
||||
columns := (select array(select column_name::text
|
||||
from information_schema.columns
|
||||
where table_name = tableName));
|
||||
return array_to_string(columns, ', ');
|
||||
end; $$
|
||||
--//
|
@@ -10,10 +10,10 @@
|
||||
This function will be overwritten by later changesets.
|
||||
*/
|
||||
create procedure contextDefined(
|
||||
currentTask varchar,
|
||||
currentRequest varchar,
|
||||
currentUser varchar,
|
||||
assumedRoles varchar
|
||||
currentTask varchar(127),
|
||||
currentRequest text,
|
||||
currentUser varchar(63),
|
||||
assumedRoles varchar(1023)
|
||||
)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
@@ -23,22 +23,27 @@ end; $$;
|
||||
Defines the transaction context.
|
||||
*/
|
||||
create or replace procedure defineContext(
|
||||
currentTask varchar,
|
||||
currentRequest varchar = null,
|
||||
currentUser varchar = null,
|
||||
assumedRoles varchar = null
|
||||
currentTask varchar(127),
|
||||
currentRequest text = null,
|
||||
currentUser varchar(63) = null,
|
||||
assumedRoles varchar(1023) = null
|
||||
)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
currentTask := coalesce(currentTask, '');
|
||||
assert length(currentTask) <= 127, FORMAT('currentTask must not be longer than 127 characters: "%s"', currentTask);
|
||||
assert length(currentTask) >= 12, FORMAT('currentTask must be at least 12 characters long: "%s""', currentTask);
|
||||
execute format('set local hsadminng.currentTask to %L', currentTask);
|
||||
|
||||
currentRequest := coalesce(currentRequest, '');
|
||||
execute format('set local hsadminng.currentRequest to %L', currentRequest);
|
||||
|
||||
currentUser := coalesce(currentUser, '');
|
||||
assert length(currentUser) <= 63, FORMAT('currentUser must not be longer than 63 characters: "%s"', currentUser);
|
||||
execute format('set local hsadminng.currentUser to %L', currentUser);
|
||||
|
||||
assumedRoles := coalesce(assumedRoles, '');
|
||||
assert length(assumedRoles) <= 1023, FORMAT('assumedRoles must not be longer than 1023 characters: "%s"', assumedRoles);
|
||||
execute format('set local hsadminng.assumedRoles to %L', assumedRoles);
|
||||
|
||||
call contextDefined(currentTask, currentRequest, currentUser, assumedRoles);
|
||||
@@ -54,11 +59,11 @@ end; $$;
|
||||
Raises exception if not set.
|
||||
*/
|
||||
create or replace function currentTask()
|
||||
returns varchar(96)
|
||||
returns varchar(127)
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
currentTask varchar(96);
|
||||
currentTask varchar(127);
|
||||
begin
|
||||
begin
|
||||
currentTask := current_setting('hsadminng.currentTask');
|
||||
@@ -82,11 +87,11 @@ end; $$;
|
||||
Raises exception if not set.
|
||||
*/
|
||||
create or replace function currentRequest()
|
||||
returns varchar(512)
|
||||
returns text
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
currentRequest varchar(512);
|
||||
currentRequest text;
|
||||
begin
|
||||
begin
|
||||
currentRequest := current_setting('hsadminng.currentRequest');
|
||||
@@ -130,22 +135,11 @@ end; $$;
|
||||
or empty array, if not set.
|
||||
*/
|
||||
create or replace function assumedRoles()
|
||||
returns varchar(63)[]
|
||||
returns varchar(1023)[]
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
currentSubject varchar(63);
|
||||
begin
|
||||
begin
|
||||
currentSubject := current_setting('hsadminng.assumedRoles');
|
||||
exception
|
||||
when others then
|
||||
return array []::varchar[];
|
||||
end;
|
||||
if (currentSubject = '') then
|
||||
return array []::varchar[];
|
||||
end if;
|
||||
return string_to_array(currentSubject, ';');
|
||||
return string_to_array(current_setting('hsadminng.assumedRoles', true), ';');
|
||||
end; $$;
|
||||
|
||||
create or replace function cleanIdentifier(rawIdentifier varchar)
|
||||
@@ -155,7 +149,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar)
|
||||
declare
|
||||
cleanIdentifier varchar;
|
||||
begin
|
||||
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._:]+', '', 'g');
|
||||
cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._]+', '', 'g');
|
||||
return cleanIdentifier;
|
||||
end; $$;
|
||||
|
||||
@@ -213,17 +207,17 @@ begin
|
||||
end ; $$;
|
||||
|
||||
create or replace function currentSubjects()
|
||||
returns varchar(63)[]
|
||||
returns varchar(1023)[]
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
assumedRoles varchar(63)[];
|
||||
assumedRoles varchar(1023)[];
|
||||
begin
|
||||
assumedRoles := assumedRoles();
|
||||
if array_length(assumedRoles, 1) > 0 then
|
||||
return assumedRoles();
|
||||
return assumedRoles;
|
||||
else
|
||||
return array [currentUser()]::varchar(63)[];
|
||||
return array [currentUser()]::varchar(1023)[];
|
||||
end if;
|
||||
end; $$;
|
||||
|
@@ -27,9 +27,9 @@ create table tx_context
|
||||
txId bigint not null,
|
||||
txTimestamp timestamp not null,
|
||||
currentUser varchar(63) not null, -- not the uuid, because users can be deleted
|
||||
assumedRoles varchar not null, -- not the uuids, because roles can be deleted
|
||||
currentTask varchar(96) not null,
|
||||
currentRequest varchar(512) not null
|
||||
assumedRoles varchar(1023) not null, -- not the uuids, because roles can be deleted
|
||||
currentTask varchar(127) not null,
|
||||
currentRequest text not null
|
||||
);
|
||||
|
||||
create index on tx_context using brin (txTimestamp);
|
@@ -1,88 +0,0 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
-- PERMISSIONS
|
||||
--changeset rbac-role-builder-to-uuids:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace function toPermissionUuids(forObjectUuid uuid, permitOps RbacOp[])
|
||||
returns uuid[]
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
return createPermissions(forObjectUuid, permitOps);
|
||||
end; $$;
|
||||
|
||||
create or replace function toRoleUuids(roleDescriptors RbacRoleDescriptor[])
|
||||
returns uuid[]
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
superRoleDescriptor RbacRoleDescriptor;
|
||||
superRoleUuids uuid[] := array []::uuid[];
|
||||
begin
|
||||
foreach superRoleDescriptor in array roleDescriptors
|
||||
loop
|
||||
if superRoleDescriptor is not null then
|
||||
superRoleUuids := superRoleUuids || getRoleId(superRoleDescriptor, 'fail');
|
||||
end if;
|
||||
end loop;
|
||||
|
||||
return superRoleUuids;
|
||||
end; $$;
|
||||
|
||||
|
||||
-- =================================================================
|
||||
-- CREATE ROLE
|
||||
--changeset rbac-role-builder-create-role:1 endDelimiter:--//
|
||||
-- -----------------------------------------------------------------
|
||||
|
||||
create or replace function createRoleWithGrants(
|
||||
roleDescriptor RbacRoleDescriptor,
|
||||
permissions RbacOp[] = array[]::RbacOp[],
|
||||
incomingSuperRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
|
||||
outgoingSubRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
|
||||
userUuids uuid[] = array[]::uuid[],
|
||||
grantedByRole RbacRoleDescriptor = null
|
||||
)
|
||||
returns uuid
|
||||
called on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
roleUuid uuid;
|
||||
superRoleUuid uuid;
|
||||
subRoleUuid uuid;
|
||||
userUuid uuid;
|
||||
grantedByRoleUuid uuid;
|
||||
begin
|
||||
roleUuid := createRole(roleDescriptor);
|
||||
|
||||
if cardinality(permissions) >0 then
|
||||
call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions));
|
||||
end if;
|
||||
|
||||
foreach superRoleUuid in array toRoleUuids(incomingSuperRoles)
|
||||
loop
|
||||
call grantRoleToRole(roleUuid, superRoleUuid);
|
||||
end loop;
|
||||
|
||||
foreach subRoleUuid in array toRoleUuids(outgoingSubRoles)
|
||||
loop
|
||||
call grantRoleToRole(subRoleUuid, roleUuid);
|
||||
end loop;
|
||||
|
||||
if cardinality(userUuids) > 0 then
|
||||
if grantedByRole is null then
|
||||
raise exception 'to directly assign users to roles, grantingRole has to be given';
|
||||
end if;
|
||||
grantedByRoleUuid := getRoleId(grantedByRole, 'fail');
|
||||
foreach userUuid in array userUuids
|
||||
loop
|
||||
call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid);
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
return roleUuid;
|
||||
end; $$;
|
||||
--//
|
||||
|
@@ -86,29 +86,6 @@ create or replace function findRbacUserId(userName varchar)
|
||||
language sql as $$
|
||||
select uuid from RbacUser where name = userName
|
||||
$$;
|
||||
|
||||
create type RbacWhenNotExists as enum ('fail', 'create');
|
||||
|
||||
create or replace function getRbacUserId(userName varchar, whenNotExists RbacWhenNotExists)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
userUuid uuid;
|
||||
begin
|
||||
userUuid = findRbacUserId(userName);
|
||||
if (userUuid is null) then
|
||||
if (whenNotExists = 'fail') then
|
||||
raise exception 'RbacUser with name="%" not found', userName;
|
||||
end if;
|
||||
if (whenNotExists = 'create') then
|
||||
userUuid = createRbacUser(userName);
|
||||
end if;
|
||||
end if;
|
||||
return userUuid;
|
||||
end;
|
||||
$$;
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
@@ -187,7 +164,7 @@ end; $$;
|
||||
|
||||
*/
|
||||
|
||||
create type RbacRoleType as enum ('owner', 'admin', 'agent', 'tenant', 'guest');
|
||||
create type RbacRoleType as enum ('OWNER', 'ADMIN', 'AGENT', 'TENANT', 'GUEST', 'REFERRER');
|
||||
|
||||
create table RbacRole
|
||||
(
|
||||
@@ -203,15 +180,33 @@ create type RbacRoleDescriptor as
|
||||
(
|
||||
objectTable varchar(63), -- for human readability and easier debugging
|
||||
objectUuid uuid,
|
||||
roleType RbacRoleType
|
||||
roleType RbacRoleType,
|
||||
assumed boolean
|
||||
);
|
||||
|
||||
create or replace function roleDescriptor(objectTable varchar(63), objectUuid uuid, roleType RbacRoleType)
|
||||
create or replace function assumed()
|
||||
returns boolean
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select true;
|
||||
$$;
|
||||
|
||||
create or replace function unassumed()
|
||||
returns boolean
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select false;
|
||||
$$;
|
||||
|
||||
|
||||
create or replace function roleDescriptor(
|
||||
objectTable varchar(63), objectUuid uuid, roleType RbacRoleType,
|
||||
assumed boolean = true) -- just for DSL readability, belongs actually to the grant
|
||||
returns RbacRoleDescriptor
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select objectTable, objectUuid, roleType::RbacRoleType;
|
||||
select objectTable, objectUuid, roleType::RbacRoleType, assumed;
|
||||
$$;
|
||||
|
||||
create or replace function createRole(roleDescriptor RbacRoleDescriptor)
|
||||
@@ -254,7 +249,7 @@ declare
|
||||
roleUuid uuid;
|
||||
begin
|
||||
-- TODO.refact: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences
|
||||
roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), '.'));
|
||||
roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), ':'));
|
||||
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
|
||||
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
|
||||
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
|
||||
@@ -275,21 +270,17 @@ create or replace function findRoleId(roleDescriptor RbacRoleDescriptor)
|
||||
select uuid from RbacRole where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType;
|
||||
$$;
|
||||
|
||||
create or replace function getRoleId(roleDescriptor RbacRoleDescriptor, whenNotExists RbacWhenNotExists)
|
||||
create or replace function getRoleId(roleDescriptor RbacRoleDescriptor)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
roleUuid uuid;
|
||||
begin
|
||||
roleUuid = findRoleId(roleDescriptor);
|
||||
assert roleDescriptor is not null, 'roleDescriptor must not be null';
|
||||
|
||||
roleUuid := findRoleId(roleDescriptor);
|
||||
if (roleUuid is null) then
|
||||
if (whenNotExists = 'fail') then
|
||||
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
||||
end if;
|
||||
if (whenNotExists = 'create') then
|
||||
roleUuid = createRole(roleDescriptor);
|
||||
end if;
|
||||
raise exception 'RbacRole "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
|
||||
end if;
|
||||
return roleUuid;
|
||||
end;
|
||||
@@ -365,38 +356,68 @@ create trigger deleteRbacRolesOfRbacObject_Trigger
|
||||
/*
|
||||
|
||||
*/
|
||||
create domain RbacOp as varchar(67)
|
||||
create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone
|
||||
check (
|
||||
VALUE = '*'
|
||||
or VALUE = 'delete'
|
||||
or VALUE = 'edit'
|
||||
or VALUE = 'view'
|
||||
or VALUE = 'assume'
|
||||
VALUE = 'DELETE'
|
||||
or VALUE = 'UPDATE'
|
||||
or VALUE = 'SELECT'
|
||||
or VALUE = 'INSERT'
|
||||
or VALUE = 'ASSUME'
|
||||
-- TODO: all values below are deprecated, use insert with table
|
||||
or VALUE ~ '^add-[a-z]+$'
|
||||
or VALUE ~ '^new-[a-z-]+$'
|
||||
);
|
||||
|
||||
create table RbacPermission
|
||||
(
|
||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||
objectUuid uuid not null references RbacObject,
|
||||
op RbacOp not null,
|
||||
unique (objectUuid, op)
|
||||
uuid uuid primary key references RbacReference (uuid) on delete cascade,
|
||||
objectUuid uuid not null references RbacObject,
|
||||
op RbacOp not null,
|
||||
opTableName varchar(60)
|
||||
);
|
||||
|
||||
ALTER TABLE RbacPermission
|
||||
ADD CONSTRAINT RbacPermission_uc UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);
|
||||
|
||||
call create_journal('RbacPermission');
|
||||
|
||||
create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp)
|
||||
returns bool
|
||||
language sql as $$
|
||||
select exists(
|
||||
select op
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and p.op in ('*', forOp)
|
||||
);
|
||||
$$;
|
||||
create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionUuid uuid;
|
||||
begin
|
||||
if (forObjectUuid is null) then
|
||||
raise exception 'forObjectUuid must not be null';
|
||||
end if;
|
||||
if (forOp = 'INSERT' and forOpTableName is null) then
|
||||
raise exception 'INSERT permissions needs forOpTableName';
|
||||
end if;
|
||||
if (forOp <> 'INSERT' and forOpTableName is not null) then
|
||||
raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
|
||||
end if;
|
||||
|
||||
permissionUuid := (
|
||||
select uuid from RbacPermission
|
||||
where objectUuid = forObjectUuid
|
||||
and op = forOp and opTableName is not distinct from forOpTableName);
|
||||
if (permissionUuid is null) then
|
||||
insert into RbacReference ("type")
|
||||
values ('RbacPermission')
|
||||
returning uuid into permissionUuid;
|
||||
begin
|
||||
insert into RbacPermission (uuid, objectUuid, op, opTableName)
|
||||
values (permissionUuid, forObjectUuid, forOp, forOpTableName);
|
||||
exception
|
||||
when others then
|
||||
raise exception 'insert into RbacPermission (uuid, objectUuid, op, opTableName)
|
||||
values (%, %, %, %);', permissionUuid, forObjectUuid, forOp, forOpTableName;
|
||||
end;
|
||||
end if;
|
||||
return permissionUuid;
|
||||
end; $$;
|
||||
|
||||
-- TODO: deprecated, remove and amend all usages to createPermission
|
||||
create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[])
|
||||
returns uuid[]
|
||||
language plpgsql as $$
|
||||
@@ -407,9 +428,6 @@ begin
|
||||
if (forObjectUuid is null) then
|
||||
raise exception 'forObjectUuid must not be null';
|
||||
end if;
|
||||
if (array_length(permitOps, 1) > 1 and '*' = any (permitOps)) then
|
||||
raise exception '"*" operation must not be assigned along with other operations: %', permitOps;
|
||||
end if;
|
||||
|
||||
for i in array_lower(permitOps, 1)..array_upper(permitOps, 1)
|
||||
loop
|
||||
@@ -430,7 +448,19 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp)
|
||||
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select uuid
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and (forOp = 'SELECT' or p.op = forOp) -- all other RbacOp include 'SELECT'
|
||||
and p.opTableName = forOpTableName
|
||||
$$;
|
||||
|
||||
create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
@@ -439,25 +469,46 @@ select uuid
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and p.op = forOp
|
||||
and p.opTableName = forOpTableName
|
||||
$$;
|
||||
|
||||
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp)
|
||||
create or replace function getPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionId uuid;
|
||||
permissionUuid uuid;
|
||||
begin
|
||||
permissionId := findPermissionId(forObjectUuid, forOp);
|
||||
if permissionId is null and forOp <> '*' then
|
||||
permissionId := findPermissionId(forObjectUuid, '*');
|
||||
end if;
|
||||
return permissionId;
|
||||
end $$;
|
||||
|
||||
select uuid into permissionUuid
|
||||
from RbacPermission p
|
||||
where p.objectUuid = forObjectUuid
|
||||
and p.op = forOp
|
||||
and forOpTableName is null or p.opTableName = forOpTableName;
|
||||
assert permissionUuid is not null,
|
||||
format('permission %s %s for object UUID %s cannot be found', forOp, forOpTableName, forObjectUuid);
|
||||
return permissionUuid;
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-base-duplicate-role-grant-exception:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure raiseDuplicateRoleGrantException(subRoleId uuid, superRoleId uuid)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
subRoleIdName text;
|
||||
superRoleIdName text;
|
||||
begin
|
||||
select roleIdName from rbacRole_ev where uuid=subRoleId into subRoleIdName;
|
||||
select roleIdName from rbacRole_ev where uuid=superRoleId into superRoleIdName;
|
||||
raise exception '[400] Duplicate role grant detected: role % (%) already granted to % (%)', subRoleId, subRoleIdName, superRoleId, superRoleIdName;
|
||||
end;
|
||||
$$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-base-GRANTS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
@@ -552,6 +603,18 @@ select exists(
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace function hasInsertPermission(objectUuid uuid, forOp RbacOp, tableName text )
|
||||
returns BOOL
|
||||
stable -- leakproof
|
||||
language plpgsql as $$
|
||||
declare
|
||||
permissionUuid uuid;
|
||||
begin
|
||||
permissionUuid = findPermissionId(objectUuid, forOp, tableName);
|
||||
return permissionUuid is not null;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace function hasGlobalRoleGranted(userUuid uuid)
|
||||
returns bool
|
||||
stable -- leakproof
|
||||
@@ -566,6 +629,27 @@ select exists(
|
||||
);
|
||||
$$;
|
||||
|
||||
create or replace procedure grantPermissionToRole(permissionUuid uuid, roleUuid uuid)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
perform assertReferenceType('roleId (ascendant)', roleUuid, 'RbacRole');
|
||||
perform assertReferenceType('permissionId (descendant)', permissionUuid, 'RbacPermission');
|
||||
|
||||
insert
|
||||
into RbacGrants (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
|
||||
values (currentTriggerObjectUuid(), roleUuid, permissionUuid, true)
|
||||
on conflict do nothing; -- allow granting multiple times
|
||||
end;
|
||||
$$;
|
||||
|
||||
create or replace procedure grantPermissionToRole(permissionUuid uuid, roleDesc RbacRoleDescriptor)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
call grantPermissionToRole(permissionUuid, findRoleId(roleDesc));
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- TODO: deprecated, remove and use grantPermissionToRole(...)
|
||||
create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
|
||||
language plpgsql as $$
|
||||
begin
|
||||
@@ -591,7 +675,7 @@ begin
|
||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||
|
||||
if isGranted(subRoleId, superRoleId) then
|
||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
||||
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||
end if;
|
||||
|
||||
insert
|
||||
@@ -607,6 +691,11 @@ declare
|
||||
superRoleId uuid;
|
||||
subRoleId uuid;
|
||||
begin
|
||||
-- TODO: maybe separate method grantRoleToRoleIfNotNull(...) for NULLABLE references
|
||||
if superRole.objectUuid is null or subRole.objectuuid is null then
|
||||
return;
|
||||
end if;
|
||||
|
||||
superRoleId := findRoleId(superRole);
|
||||
subRoleId := findRoleId(subRole);
|
||||
|
||||
@@ -614,7 +703,7 @@ begin
|
||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||
|
||||
if isGranted(subRoleId, superRoleId) then
|
||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
||||
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||
end if;
|
||||
|
||||
insert
|
||||
@@ -629,6 +718,7 @@ declare
|
||||
superRoleId uuid;
|
||||
subRoleId uuid;
|
||||
begin
|
||||
if ( superRoleId is null ) then return; end if;
|
||||
superRoleId := findRoleId(superRole);
|
||||
if ( subRoleId is null ) then return; end if;
|
||||
subRoleId := findRoleId(subRole);
|
||||
@@ -637,7 +727,7 @@ begin
|
||||
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
|
||||
|
||||
if isGranted(subRoleId, superRoleId) then
|
||||
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
|
||||
call raiseDuplicateRoleGrantException(subRoleId, superRoleId);
|
||||
end if;
|
||||
|
||||
insert
|
||||
@@ -661,11 +751,39 @@ begin
|
||||
if (isGranted(superRoleId, subRoleId)) then
|
||||
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = subRoleId;
|
||||
else
|
||||
raise exception 'cannot revoke role % (%) from % (% because it is not granted',
|
||||
raise exception 'cannot revoke role % (%) from % (%) because it is not granted',
|
||||
subRole, subRoleId, superRole, superRoleId;
|
||||
end if;
|
||||
end; $$;
|
||||
|
||||
create or replace procedure revokePermissionFromRole(permissionId UUID, superRole RbacRoleDescriptor)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
superRoleId uuid;
|
||||
permissionOp text;
|
||||
objectTable text;
|
||||
objectUuid uuid;
|
||||
begin
|
||||
superRoleId := findRoleId(superRole);
|
||||
|
||||
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
|
||||
perform assertReferenceType('permission (descendant)', permissionId, 'RbacPermission');
|
||||
|
||||
if (isGranted(superRoleId, permissionId)) then
|
||||
delete from RbacGrants where ascendantUuid = superRoleId and descendantUuid = permissionId;
|
||||
else
|
||||
select p.op, o.objectTable, o.uuid
|
||||
from rbacGrants g
|
||||
join rbacPermission p on p.uuid=g.descendantUuid
|
||||
join rbacobject o on o.uuid=p.objectUuid
|
||||
where g.uuid=permissionId
|
||||
into permissionOp, objectTable, objectUuid;
|
||||
|
||||
raise exception 'cannot revoke permission % (% on %#% (%) from % (%)) because it is not granted',
|
||||
permissionId, permissionOp, objectTable, objectUuid, permissionId, superRole, superRoleId;
|
||||
end if;
|
||||
end; $$;
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
@@ -697,7 +815,7 @@ begin
|
||||
select descendantUuid
|
||||
from grants) as granted
|
||||
join RbacPermission perm
|
||||
on granted.descendantUuid = perm.uuid and perm.op in ('*', requiredOp)
|
||||
on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp)
|
||||
join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable
|
||||
limit maxObjects + 1;
|
||||
|
||||
@@ -789,6 +907,5 @@ do $$
|
||||
create role restricted;
|
||||
grant all privileges on all tables in schema public to restricted;
|
||||
end if;
|
||||
end $$
|
||||
end $$;
|
||||
--//
|
||||
|
@@ -30,24 +30,35 @@ begin
|
||||
insert
|
||||
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
|
||||
values (grantedByRoleUuid, userUuid, roleUuid, doAssume);
|
||||
-- TODO.spec: What should happen on mupltiple grants? What if options (doAssume) are not the same?
|
||||
-- TODO.spec: What should happen on multiple grants? What if options (doAssume) are not the same?
|
||||
-- Most powerful or latest grant wins? What about managed?
|
||||
-- on conflict do nothing; -- allow granting multiple times
|
||||
end; $$;
|
||||
|
||||
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
grantedByRoleIdName text;
|
||||
grantedRoleIdName text;
|
||||
begin
|
||||
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
|
||||
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
|
||||
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
|
||||
|
||||
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then
|
||||
raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects();
|
||||
end if;
|
||||
assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null';
|
||||
assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null';
|
||||
assert userUuid is not null, 'userUuid must not be null';
|
||||
|
||||
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then
|
||||
select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName;
|
||||
raise exception '[403] Access to granted-by-role % (%) forbidden for % (%)',
|
||||
grantedByRoleIdName, grantedByRoleUuid, currentSubjects(), currentSubjectsUuids();
|
||||
end if;
|
||||
if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then
|
||||
raise exception '[403] Access to granted role % forbidden for %', grantedRoleUuid, currentSubjects();
|
||||
select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName;
|
||||
select roleIdName from rbacRole_ev where uuid=grantedRoleUuid into grantedRoleIdName;
|
||||
raise exception '[403] Access to granted role % (%) forbidden for % (%)',
|
||||
grantedRoleIdName, grantedRoleUuid, grantedByRoleIdName, grantedByRoleUuid;
|
||||
end if;
|
||||
|
||||
insert
|
||||
@@ -99,4 +110,17 @@ begin
|
||||
where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid
|
||||
and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid;
|
||||
end; $$;
|
||||
--/
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-user-grant-REVOKE-PERMISSION-FROM-ROLE:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure revokePermissionFromRole(permissionUuid uuid, superRoleUuid uuid)
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise INFO 'delete from RbacGrants where ascendantUuid = % and descendantUuid = %', superRoleUuid, permissionUuid;
|
||||
delete from RbacGrants as g
|
||||
where g.ascendantUuid = superRoleUuid and g.descendantUuid = permissionUuid;
|
||||
end; $$;
|
||||
--//
|
@@ -50,20 +50,23 @@ begin
|
||||
|
||||
foreach roleName in array string_to_array(assumedRoles, ';')
|
||||
loop
|
||||
roleNameParts = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), '.'));
|
||||
roleNameParts = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), ':'));
|
||||
objectTableToAssume = split_part(roleNameParts, '#', 1);
|
||||
objectNameToAssume = split_part(roleNameParts, '#', 2);
|
||||
roleTypeToAssume = split_part(roleNameParts, '#', 3);
|
||||
|
||||
objectUuidToAssume = findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
|
||||
if objectUuidToAssume is null then
|
||||
raise exception '[401] object % cannot be found in table %', objectNameToAssume, objectTableToAssume;
|
||||
end if;
|
||||
|
||||
select uuid as roleuuidToAssume
|
||||
select uuid
|
||||
from RbacRole r
|
||||
where r.objectUuid = objectUuidToAssume
|
||||
and r.roleType = roleTypeToAssume
|
||||
into roleUuidToAssume;
|
||||
if roleUuidToAssume is null then
|
||||
raise exception '[403] role % not accessible for user %', roleName, currentSubjects();
|
||||
raise exception '[403] role % does not exist or is not accessible for user %', roleName, currentUser();
|
||||
end if;
|
||||
if not isGranted(currentUserUuid, roleUuidToAssume) then
|
||||
raise exception '[403] user % has no permission to assume role %', currentUser(), roleName;
|
||||
@@ -82,10 +85,10 @@ end; $$;
|
||||
This function will be overwritten by later changesets.
|
||||
*/
|
||||
create or replace procedure contextDefined(
|
||||
currentTask varchar,
|
||||
currentRequest varchar,
|
||||
currentUser varchar,
|
||||
assumedRoles varchar
|
||||
currentTask varchar(127),
|
||||
currentRequest text,
|
||||
currentUser varchar(63),
|
||||
assumedRoles varchar(1023)
|
||||
)
|
||||
language plpgsql as $$
|
||||
declare
|
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
drop view if exists rbacrole_ev;
|
||||
create or replace view rbacrole_ev as
|
||||
select (objectTable || '#' || objectIdName || '.' || roleType) as roleIdName, *
|
||||
select (objectTable || '#' || objectIdName || ':' || roleType) as roleIdName, *
|
||||
-- @formatter:off
|
||||
from (
|
||||
select r.*,
|
||||
@@ -40,7 +40,7 @@ select *
|
||||
where isGranted(currentSubjectsUuids(), r.uuid)
|
||||
) as unordered
|
||||
-- @formatter:on
|
||||
order by objectTable || '#' || objectIdName || '.' || roleType;
|
||||
order by objectTable || '#' || objectIdName || ':' || roleType;
|
||||
grant all privileges on rbacrole_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
--//
|
||||
|
||||
@@ -57,12 +57,13 @@ create or replace view rbacgrants_ev as
|
||||
-- @formatter:off
|
||||
select x.grantUuid as uuid,
|
||||
x.grantedByTriggerOf as grantedByTriggerOf,
|
||||
go.objectTable || '#' || findIdNameByObjectUuid(go.objectTable, go.uuid) || '.' || r.roletype as grantedByRoleIdName,
|
||||
go.objectTable || '#' || findIdNameByObjectUuid(go.objectTable, go.uuid) || ':' || r.roletype as grantedByRoleIdName,
|
||||
x.ascendingIdName as ascendantIdName,
|
||||
x.descendingIdName as descendantIdName,
|
||||
x.grantedByRoleUuid,
|
||||
x.ascendantUuid as ascendantUuid,
|
||||
x.descendantUuid as descendantUuid,
|
||||
x.op as permOp, x.optablename as permOpTableName,
|
||||
x.assumed
|
||||
from (
|
||||
select g.uuid as grantUuid,
|
||||
@@ -70,17 +71,21 @@ create or replace view rbacgrants_ev as
|
||||
g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
|
||||
|
||||
coalesce(
|
||||
'user ' || au.name,
|
||||
'role ' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || '.' || ar.roletype
|
||||
'user:' || au.name,
|
||||
'role:' || aro.objectTable || '#' || findIdNameByObjectUuid(aro.objectTable, aro.uuid) || ':' || ar.roletype
|
||||
) as ascendingIdName,
|
||||
aro.objectTable, aro.uuid,
|
||||
|
||||
coalesce(
|
||||
'role ' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || '.' || dr.roletype,
|
||||
'perm ' || dp.op || ' on ' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid)
|
||||
( case
|
||||
when dro is not null
|
||||
then ('role:' || dro.objectTable || '#' || findIdNameByObjectUuid(dro.objectTable, dro.uuid) || ':' || dr.roletype)
|
||||
when dp.op = 'INSERT'
|
||||
then 'perm:' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid) || ':' || dp.op || '>' || dp.opTableName
|
||||
else 'perm:' || dpo.objecttable || '#' || findIdNameByObjectUuid(dpo.objectTable, dpo.uuid) || ':' || dp.op
|
||||
end
|
||||
) as descendingIdName,
|
||||
dro.objectTable, dro.uuid
|
||||
from rbacgrants as g
|
||||
dro.objectTable, dro.uuid,
|
||||
dp.op, dp.optablename
|
||||
from rbacgrants as g
|
||||
|
||||
left outer join rbacrole as ar on ar.uuid = g.ascendantUuid
|
||||
left outer join rbacobject as aro on aro.uuid = ar.objectuuid
|
||||
@@ -110,8 +115,8 @@ create or replace view rbacgrants_ev as
|
||||
drop view if exists rbacgrants_rv;
|
||||
create or replace view rbacgrants_rv as
|
||||
-- @formatter:off
|
||||
select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || '.' || r.roletype as grantedByRoleIdName,
|
||||
g.objectTable || '#' || g.objectIdName || '.' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
|
||||
select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || ':' || r.roletype as grantedByRoleIdName,
|
||||
g.objectTable || '#' || g.objectIdName || ':' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
|
||||
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
|
||||
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
|
||||
from (
|
||||
@@ -322,7 +327,7 @@ execute function deleteRbacUser();
|
||||
drop view if exists RbacOwnGrantedPermissions_rv;
|
||||
create or replace view RbacOwnGrantedPermissions_rv as
|
||||
select r.uuid as roleuuid, p.uuid as permissionUuid,
|
||||
(r.objecttable || '#' || r.objectidname || '.' || r.roletype) as roleName, p.op,
|
||||
(r.objecttable || ':' || r.objectidname || ':' || r.roletype) as roleName, p.op,
|
||||
o.objecttable, r.objectidname, o.uuid as objectuuid
|
||||
from rbacrole_rv r
|
||||
join rbacgrants g on g.ascendantuuid = r.uuid
|
||||
@@ -337,11 +342,9 @@ grant all privileges on RbacOwnGrantedPermissions_rv to ${HSADMINNG_POSTGRES_RES
|
||||
/*
|
||||
Returns all permissions granted to the given user,
|
||||
which are also visible to the current user or assumed roles.
|
||||
|
||||
|
||||
*/
|
||||
create or replace function grantedPermissions(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid)
|
||||
*/
|
||||
create or replace function grantedPermissionsRaw(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid)
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
@@ -356,12 +359,14 @@ begin
|
||||
|
||||
return query select
|
||||
xp.roleUuid,
|
||||
(xp.roleObjectTable || '#' || xp.roleObjectIdName || '.' || xp.roleType) as roleName,
|
||||
xp.permissionUuid, xp.op, xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
||||
(xp.roleObjectTable || '#' || xp.roleObjectIdName || ':' || xp.roleType) as roleName,
|
||||
xp.permissionUuid, xp.op, xp.opTableName,
|
||||
xp.permissionObjectTable, xp.permissionObjectIdName, xp.permissionObjectUuid
|
||||
from (select
|
||||
r.uuid as roleUuid, r.roletype, ro.objectTable as roleObjectTable,
|
||||
findIdNameByObjectUuid(ro.objectTable, ro.uuid) as roleObjectIdName,
|
||||
p.uuid as permissionUuid, p.op, po.objecttable as permissionObjectTable,
|
||||
p.uuid as permissionUuid, p.op, p.opTableName,
|
||||
po.objecttable as permissionObjectTable,
|
||||
findIdNameByObjectUuid(po.objectTable, po.uuid) as permissionObjectIdName,
|
||||
po.uuid as permissionObjectUuid
|
||||
from queryPermissionsGrantedToSubjectId( targetUserUuid) as p
|
||||
@@ -373,4 +378,15 @@ begin
|
||||
) xp;
|
||||
-- @formatter:on
|
||||
end; $$;
|
||||
|
||||
create or replace function grantedPermissions(targetUserUuid uuid)
|
||||
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, opTableName varchar(60), objectTable varchar(60), objectIdName varchar, objectUuid uuid)
|
||||
returns null on null input
|
||||
language sql as $$
|
||||
select * from grantedPermissionsRaw(targetUserUuid)
|
||||
union all
|
||||
select roleUuid, roleName, permissionUuid, 'SELECT'::RbacOp, opTableName, objectTable, objectIdName, objectUuid
|
||||
from grantedPermissionsRaw(targetUserUuid)
|
||||
where op <> 'SELECT'::RbacOp;
|
||||
$$;
|
||||
--//
|
@@ -0,0 +1,67 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
|
||||
-- =================================================================
|
||||
-- CREATE ROLE
|
||||
--changeset rbac-role-builder-create-role:1 endDelimiter:--//
|
||||
-- -----------------------------------------------------------------
|
||||
|
||||
create or replace function createRoleWithGrants(
|
||||
roleDescriptor RbacRoleDescriptor,
|
||||
permissions RbacOp[] = array[]::RbacOp[],
|
||||
incomingSuperRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
|
||||
outgoingSubRoles RbacRoleDescriptor[] = array[]::RbacRoleDescriptor[],
|
||||
userUuids uuid[] = array[]::uuid[],
|
||||
grantedByRole RbacRoleDescriptor = null
|
||||
)
|
||||
returns uuid
|
||||
called on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
roleUuid uuid;
|
||||
permission RbacOp;
|
||||
permissionUuid uuid;
|
||||
subRoleDesc RbacRoleDescriptor;
|
||||
superRoleDesc RbacRoleDescriptor;
|
||||
subRoleUuid uuid;
|
||||
superRoleUuid uuid;
|
||||
userUuid uuid;
|
||||
userGrantsByRoleUuid uuid;
|
||||
begin
|
||||
roleUuid := createRole(roleDescriptor);
|
||||
|
||||
foreach permission in array permissions
|
||||
loop
|
||||
permissionUuid := createPermission(roleDescriptor.objectuuid, permission);
|
||||
call grantPermissionToRole(permissionUuid, roleUuid);
|
||||
end loop;
|
||||
|
||||
foreach superRoleDesc in array array_remove(incomingSuperRoles, null)
|
||||
loop
|
||||
superRoleUuid := getRoleId(superRoleDesc);
|
||||
call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed);
|
||||
end loop;
|
||||
|
||||
foreach subRoleDesc in array array_remove(outgoingSubRoles, null)
|
||||
loop
|
||||
subRoleUuid := getRoleId(subRoleDesc);
|
||||
call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed);
|
||||
end loop;
|
||||
|
||||
if cardinality(userUuids) > 0 then
|
||||
-- direct grants to users need a grantedByRole which can revoke the grant
|
||||
if grantedByRole is null then
|
||||
userGrantsByRoleUuid := roleUuid; -- TODO.spec: or do we want to require an explicit userGrantsByRoleUuid?
|
||||
else
|
||||
userGrantsByRoleUuid := getRoleId(grantedByRole);
|
||||
end if;
|
||||
foreach userUuid in array userUuids
|
||||
loop
|
||||
call grantRoleToUserUnchecked(userGrantsByRoleUuid, roleUuid, userUuid);
|
||||
end loop;
|
||||
end if;
|
||||
|
||||
return roleUuid;
|
||||
end; $$;
|
||||
--//
|
||||
|
@@ -13,8 +13,7 @@ declare
|
||||
begin
|
||||
createInsertTriggerSQL = format($sql$
|
||||
create trigger createRbacObjectFor_%s_Trigger
|
||||
before insert
|
||||
on %s
|
||||
before insert on %s
|
||||
for each row
|
||||
execute procedure insertRelatedRbacObject();
|
||||
$sql$, targetTable, targetTable);
|
||||
@@ -36,50 +35,59 @@ end; $$;
|
||||
--changeset rbac-generators-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure generateRbacRoleDescriptors(prefix text, targetTable text)
|
||||
create procedure generateRbacRoleDescriptors(prefix text, targetTable text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sql text;
|
||||
begin
|
||||
sql = format($sql$
|
||||
create or replace function %1$sOwner(entity %2$s)
|
||||
create or replace function %1$sOwner(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'owner');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'OWNER', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sAdmin(entity %2$s)
|
||||
create or replace function %1$sAdmin(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'admin');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'ADMIN', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sAgent(entity %2$s)
|
||||
create or replace function %1$sAgent(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'agent');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'AGENT', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sTenant(entity %2$s)
|
||||
create or replace function %1$sTenant(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'tenant');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'TENANT', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sGuest(entity %2$s)
|
||||
-- TODO: remove guest role
|
||||
create or replace function %1$sGuest(entity %2$s, assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'guest');
|
||||
return roleDescriptor('%2$s', entity.uuid, 'GUEST', assumed);
|
||||
end; $f$;
|
||||
|
||||
create or replace function %1$sReferrer(entity %2$s)
|
||||
returns RbacRoleDescriptor
|
||||
language plpgsql
|
||||
strict as $f$
|
||||
begin
|
||||
return roleDescriptor('%2$s', entity.uuid, 'REFERRER');
|
||||
end; $f$;
|
||||
|
||||
$sql$, prefix, targetTable);
|
||||
@@ -92,7 +100,7 @@ end; $$;
|
||||
--changeset rbac-generators-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure generateRbacIdentityView(targetTable text, idNameExpression text)
|
||||
create or replace procedure generateRbacIdentityViewFromQuery(targetTable text, sqlQuery text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sql text;
|
||||
@@ -101,11 +109,9 @@ begin
|
||||
|
||||
-- create a view to the target main table which maps an idName to the objectUuid
|
||||
sql = format($sql$
|
||||
create or replace view %1$s_iv as
|
||||
select target.uuid, cleanIdentifier(%2$s) as idName
|
||||
from %1$s as target;
|
||||
create or replace view %1$s_iv as %2$s;
|
||||
grant all privileges on %1$s_iv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
$sql$, targetTable, idNameExpression);
|
||||
$sql$, targetTable, sqlQuery);
|
||||
execute sql;
|
||||
|
||||
-- creates a function which maps an idName to the objectUuid
|
||||
@@ -130,6 +136,20 @@ begin
|
||||
$sql$, targetTable);
|
||||
execute sql;
|
||||
end; $$;
|
||||
|
||||
create or replace procedure generateRbacIdentityViewFromProjection(targetTable text, sqlProjection text)
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sqlQuery text;
|
||||
begin
|
||||
targettable := lower(targettable);
|
||||
|
||||
sqlQuery = format($sql$
|
||||
select target.uuid, cleanIdentifier(%2$s) as idName
|
||||
from %1$s as target;
|
||||
$sql$, targetTable, sqlProjection);
|
||||
call generateRbacIdentityViewFromQuery(targetTable, sqlQuery);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
@@ -137,21 +157,25 @@ end; $$;
|
||||
--changeset rbac-generators-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null)
|
||||
create or replace procedure generateRbacRestrictedView(targetTable text, orderBy text, columnUpdates text = null, columnNames text = '*')
|
||||
language plpgsql as $$
|
||||
declare
|
||||
sql text;
|
||||
newColumns text;
|
||||
begin
|
||||
targetTable := lower(targetTable);
|
||||
if columnNames = '*' then
|
||||
columnNames := columnsNames(targetTable);
|
||||
end if;
|
||||
|
||||
/*
|
||||
Creates a restricted view based on the 'view' permission of the current subject.
|
||||
Creates a restricted view based on the 'SELECT' permission of the current subject.
|
||||
*/
|
||||
sql := format($sql$
|
||||
set session session authorization default;
|
||||
create view %1$s_rv as
|
||||
with accessibleObjects as (
|
||||
select queryAccessibleObjectUuidsOfSubjectIds('view', '%1$s', currentSubjectsUuids())
|
||||
select queryAccessibleObjectUuidsOfSubjectIds('SELECT', '%1$s', currentSubjectsUuids())
|
||||
)
|
||||
select target.*
|
||||
from %1$s as target
|
||||
@@ -164,20 +188,21 @@ begin
|
||||
/**
|
||||
Instead of insert trigger function for the restricted view.
|
||||
*/
|
||||
newColumns := 'new.' || replace(columnNames, ',', ', new.');
|
||||
sql := format($sql$
|
||||
create or replace function %1$sInsert()
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
declare
|
||||
newTargetRow %1$s;
|
||||
begin
|
||||
insert
|
||||
into %1$s
|
||||
values (new.*)
|
||||
returning * into newTargetRow;
|
||||
return newTargetRow;
|
||||
end; $f$;
|
||||
$sql$, targetTable);
|
||||
create or replace function %1$sInsert()
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
declare
|
||||
newTargetRow %1$s;
|
||||
begin
|
||||
insert
|
||||
into %1$s (%2$s)
|
||||
values (%3$s)
|
||||
returning * into newTargetRow;
|
||||
return newTargetRow;
|
||||
end; $f$;
|
||||
$sql$, targetTable, columnNames, newColumns);
|
||||
execute sql;
|
||||
|
||||
/*
|
||||
@@ -200,7 +225,7 @@ begin
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
begin
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('delete', '%1$s', currentSubjectsUuids())) then
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('DELETE', '%1$s', currentSubjectsUuids())) then
|
||||
delete from %1$s p where p.uuid = old.uuid;
|
||||
return old;
|
||||
end if;
|
||||
@@ -223,7 +248,7 @@ begin
|
||||
|
||||
/**
|
||||
Instead of update trigger function for the restricted view
|
||||
based on the 'edit' permission of the current subject.
|
||||
based on the 'UPDATE' permission of the current subject.
|
||||
*/
|
||||
if columnUpdates is not null then
|
||||
sql := format($sql$
|
||||
@@ -231,7 +256,7 @@ begin
|
||||
returns trigger
|
||||
language plpgsql as $f$
|
||||
begin
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('edit', '%1$s', currentSubjectsUuids())) then
|
||||
if old.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('UPDATE', '%1$s', currentSubjectsUuids())) then
|
||||
update %1$s
|
||||
set %2$s
|
||||
where uuid = old.uuid;
|
@@ -22,6 +22,19 @@ grant select on global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-IS-GLOBAL-ADMIN:1 endDelimiter:--//
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
create or replace function isGlobalAdmin()
|
||||
returns boolean
|
||||
language plpgsql as $$
|
||||
begin
|
||||
return isGranted(currentSubjectsUuids(), findRoleId(globalAdmin()));
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-HAS-GLOBAL-PERMISSION:1 endDelimiter:--//
|
||||
-- ------------------------------------------------------------------
|
||||
@@ -96,18 +109,41 @@ commit;
|
||||
/*
|
||||
A global administrator role.
|
||||
*/
|
||||
create or replace function globalAdmin()
|
||||
create or replace function globalAdmin(assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'admin'::RbacRoleType;
|
||||
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'ADMIN'::RbacRoleType, assumed;
|
||||
$$;
|
||||
|
||||
begin transaction;
|
||||
call defineContext('creating global admin role', null, null, null);
|
||||
select createRole(globalAdmin());
|
||||
call defineContext('creating role:global#global:ADMIN', null, null, null);
|
||||
select createRole(globalAdmin());
|
||||
commit;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-GUEST-ROLE:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
A global guest role.
|
||||
*/
|
||||
create or replace function globalGuest(assumed boolean = true)
|
||||
returns RbacRoleDescriptor
|
||||
returns null on null input
|
||||
stable -- leakproof
|
||||
language sql as $$
|
||||
select 'global', (select uuid from RbacObject where objectTable = 'global'), 'GUEST'::RbacRoleType, assumed;
|
||||
$$;
|
||||
|
||||
begin transaction;
|
||||
call defineContext('creating role:global#global:guest', null, null, null);
|
||||
select createRole(globalGuest());
|
||||
commit;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset rbac-global-ADMIN-USERS:1 context:dev,tc endDelimiter:--//
|
@@ -1,145 +0,0 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('test_customer');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testCustomer', 'test_customer');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles and their assignments for a new customer for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace function createRbacRolesForTestCustomer()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
testCustomerOwnerUuid uuid;
|
||||
customerAdminUuid uuid;
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
-- the owner role with full access for Hostsharing administrators
|
||||
testCustomerOwnerUuid = createRoleWithGrants(
|
||||
testCustomerOwner(NEW),
|
||||
permissions => array['*'],
|
||||
incomingSuperRoles => array[globalAdmin()]
|
||||
);
|
||||
|
||||
-- the admin role for the customer's admins, who can view and add products
|
||||
customerAdminUuid = createRoleWithGrants(
|
||||
testCustomerAdmin(NEW),
|
||||
permissions => array['view', 'add-package'],
|
||||
-- NO auto assume for customer owner to avoid exploding permissions for administrators
|
||||
userUuids => array[getRbacUserId(NEW.adminUserName, 'create')], -- implicitly ignored if null
|
||||
grantedByRole => globalAdmin()
|
||||
);
|
||||
|
||||
-- allow the customer owner role (thus administrators) to assume the customer admin role
|
||||
call grantRoleToRole(customerAdminUuid, testCustomerOwnerUuid, false);
|
||||
|
||||
-- the tenant role which later can be used by owners+admins of sub-objects
|
||||
perform createRoleWithGrants(
|
||||
testCustomerTenant(NEW),
|
||||
permissions => array['view']
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new customer.
|
||||
*/
|
||||
|
||||
drop trigger if exists createRbacRolesForTestCustomer_Trigger on test_customer;
|
||||
create trigger createRbacRolesForTestCustomer_Trigger
|
||||
after insert
|
||||
on test_customer
|
||||
for each row
|
||||
execute procedure createRbacRolesForTestCustomer();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_customer', $idName$
|
||||
target.prefix
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('test_customer', 'target.prefix',
|
||||
$updates$
|
||||
reference = new.reference,
|
||||
prefix = new.prefix,
|
||||
adminUserName = new.adminUserName
|
||||
$updates$);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ADD-CUSTOMER:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates a global permission for add-customer and assigns it to the hostsharing admins role.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
addCustomerPermissions uuid[];
|
||||
globalObjectUuid uuid;
|
||||
globalAdminRoleUuid uuid ;
|
||||
begin
|
||||
call defineContext('granting global add-customer permission to global admin role', null, null, null);
|
||||
|
||||
globalAdminRoleUuid := findRoleId(globalAdmin());
|
||||
globalObjectUuid := (select uuid from global);
|
||||
addCustomerPermissions := createPermissions(globalObjectUuid, array ['add-customer']);
|
||||
call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions);
|
||||
end;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Used by the trigger to prevent the add-customer to current user respectively assumed roles.
|
||||
*/
|
||||
create or replace function addTestCustomerNotAllowedForCurrentSubjects()
|
||||
returns trigger
|
||||
language PLPGSQL
|
||||
as $$
|
||||
begin
|
||||
raise exception '[403] add-customer not permitted for %',
|
||||
array_to_string(currentSubjects(), ';', 'null');
|
||||
end; $$;
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to add a new customer.
|
||||
*/
|
||||
create trigger test_customer_insert_trigger
|
||||
before insert
|
||||
on test_customer
|
||||
for each row
|
||||
when ( not hasGlobalPermission('add-customer') )
|
||||
execute procedure addTestCustomerNotAllowedForCurrentSubjects();
|
||||
--//
|
||||
|
@@ -1,109 +0,0 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('test_package');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testPackage', 'test_package');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates the roles and their assignments for a new package for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
create or replace function createRbacRolesForTestPackage()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
parentCustomer test_customer;
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
select * from test_customer as c where c.uuid = NEW.customerUuid into parentCustomer;
|
||||
|
||||
-- an owner role is created and assigned to the customer's admin role
|
||||
perform createRoleWithGrants(
|
||||
testPackageOwner(NEW),
|
||||
permissions => array ['*'],
|
||||
incomingSuperRoles => array[testCustomerAdmin(parentCustomer)]
|
||||
);
|
||||
|
||||
-- an owner role is created and assigned to the package owner role
|
||||
perform createRoleWithGrants(
|
||||
testPackageAdmin(NEW),
|
||||
permissions => array ['add-domain'],
|
||||
incomingSuperRoles => array[testPackageOwner(NEW)]
|
||||
);
|
||||
|
||||
-- and a package tenant role is created and assigned to the package admin as well
|
||||
perform createRoleWithGrants(
|
||||
testPackageTenant(NEW),
|
||||
permissions => array['view'],
|
||||
incomingsuperroles => array[testPackageAdmin(NEW)],
|
||||
outgoingSubRoles => array[testCustomerTenant(parentCustomer)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new package.
|
||||
*/
|
||||
|
||||
create trigger createRbacRolesForTestPackage_Trigger
|
||||
after insert
|
||||
on test_package
|
||||
for each row
|
||||
execute procedure createRbacRolesForTestPackage();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_package', 'target.name');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates a view to the customer main table which maps the identifying name
|
||||
(in this case, the prefix) to the objectUuid.
|
||||
*/
|
||||
-- drop view if exists test_package_rv;
|
||||
-- create or replace view test_package_rv as
|
||||
-- select target.*
|
||||
-- from test_package as target
|
||||
-- where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'test_package', currentSubjectsUuids()))
|
||||
-- order by target.name;
|
||||
-- grant all privileges on test_package_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
|
||||
call generateRbacRestrictedView('test_package', 'target.name',
|
||||
$updates$
|
||||
version = new.version,
|
||||
customerUuid = new.customerUuid,
|
||||
name = new.name,
|
||||
description = new.description
|
||||
$updates$);
|
||||
|
||||
--//
|
||||
|
||||
|
@@ -1,117 +0,0 @@
|
||||
--liquibase formatted sql
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('test_domain');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testDomain', 'test_domain');
|
||||
|
||||
create or replace function createTestDomainTenantRoleIfNotExists(domain test_domain)
|
||||
returns uuid
|
||||
returns null on null input
|
||||
language plpgsql as $$
|
||||
declare
|
||||
domainTenantRoleDesc RbacRoleDescriptor;
|
||||
domainTenantRoleUuid uuid;
|
||||
begin
|
||||
domainTenantRoleDesc = testdomainTenant(domain);
|
||||
domainTenantRoleUuid = findRoleId(domainTenantRoleDesc);
|
||||
if domainTenantRoleUuid is not null then
|
||||
return domainTenantRoleUuid;
|
||||
end if;
|
||||
|
||||
return createRoleWithGrants(
|
||||
domainTenantRoleDesc,
|
||||
permissions => array['view'],
|
||||
incomingSuperRoles => array[testdomainAdmin(domain)]
|
||||
);
|
||||
end; $$;
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-ROLES-CREATION:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
/*
|
||||
Creates the roles and their assignments for a new domain for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace function createRbacRulesForTestDomain()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
declare
|
||||
parentPackage test_package;
|
||||
begin
|
||||
if TG_OP <> 'INSERT' then
|
||||
raise exception 'invalid usage of TRIGGER AFTER INSERT';
|
||||
end if;
|
||||
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
select * from test_package where uuid = NEW.packageUuid into parentPackage;
|
||||
|
||||
-- an owner role is created and assigned to the package's admin group
|
||||
perform createRoleWithGrants(
|
||||
testDomainOwner(NEW),
|
||||
permissions => array['*'],
|
||||
incomingSuperRoles => array[testPackageAdmin(parentPackage)]
|
||||
);
|
||||
|
||||
-- and a domain admin role is created and assigned to the domain owner as well
|
||||
perform createRoleWithGrants(
|
||||
testDomainAdmin(NEW),
|
||||
permissions => array['edit'],
|
||||
incomingSuperRoles => array[testDomainOwner(NEW)],
|
||||
outgoingSubRoles => array[testPackageTenant(parentPackage)]
|
||||
);
|
||||
|
||||
-- a tenent role is only created on demand
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
|
||||
/*
|
||||
An AFTER INSERT TRIGGER which creates the role structure for a new domain.
|
||||
*/
|
||||
drop trigger if exists createRbacRulesForTestDomain_Trigger on test_domain;
|
||||
create trigger createRbacRulesForTestDomain_Trigger
|
||||
after insert
|
||||
on test_domain
|
||||
for each row
|
||||
execute procedure createRbacRulesForTestDomain();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacIdentityView('test_domain', $idName$
|
||||
target.name
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-domain-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates a view to the customer main table which maps the identifying name
|
||||
(in this case, the prefix) to the objectUuid.
|
||||
*/
|
||||
drop view if exists test_domain_rv;
|
||||
create or replace view test_domain_rv as
|
||||
select target.*
|
||||
from test_domain as target
|
||||
where target.uuid in (select queryAccessibleObjectUuidsOfSubjectIds('view', 'domain', currentSubjectsUuids()));
|
||||
grant all privileges on test_domain_rv to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
|
||||
--//
|
@@ -0,0 +1,45 @@
|
||||
### rbac customer
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
|
||||
|
||||
```mermaid
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
|
||||
subgraph customer["`**customer**`"]
|
||||
direction TB
|
||||
style customer fill:#dd4901,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph customer:roles[ ]
|
||||
style customer:roles fill:#dd4901,stroke:white
|
||||
|
||||
role:customer:OWNER[[customer:OWNER]]
|
||||
role:customer:ADMIN[[customer:ADMIN]]
|
||||
role:customer:TENANT[[customer:TENANT]]
|
||||
end
|
||||
|
||||
subgraph customer:permissions[ ]
|
||||
style customer:permissions fill:#dd4901,stroke:white
|
||||
|
||||
perm:customer:INSERT{{customer:INSERT}}
|
||||
perm:customer:DELETE{{customer:DELETE}}
|
||||
perm:customer:UPDATE{{customer:UPDATE}}
|
||||
perm:customer:SELECT{{customer:SELECT}}
|
||||
end
|
||||
end
|
||||
|
||||
%% granting roles to users
|
||||
user:creator ==>|XX| role:customer:OWNER
|
||||
|
||||
%% granting roles to roles
|
||||
role:global:ADMIN ==>|XX| role:customer:OWNER
|
||||
role:customer:OWNER ==> role:customer:ADMIN
|
||||
role:customer:ADMIN ==> role:customer:TENANT
|
||||
|
||||
%% granting permissions to roles
|
||||
role:global:ADMIN ==> perm:customer:INSERT
|
||||
role:customer:OWNER ==> perm:customer:DELETE
|
||||
role:customer:ADMIN ==> perm:customer:UPDATE
|
||||
role:customer:TENANT ==> perm:customer:SELECT
|
||||
|
||||
```
|
@@ -0,0 +1,163 @@
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('test_customer');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testCustomer', 'test_customer');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-insert-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace procedure buildRbacSystemForTestCustomer(
|
||||
NEW test_customer
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testCustomerOWNER(NEW),
|
||||
permissions => array['DELETE'],
|
||||
incomingSuperRoles => array[globalADMIN(unassumed())],
|
||||
userUuids => array[currentUserUuid()]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testCustomerADMIN(NEW),
|
||||
permissions => array['UPDATE'],
|
||||
incomingSuperRoles => array[testCustomerOWNER(NEW)]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testCustomerTENANT(NEW),
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[testCustomerADMIN(NEW)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new test_customer row.
|
||||
*/
|
||||
|
||||
create or replace function insertTriggerForTestCustomer_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call buildRbacSystemForTestCustomer(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger insertTriggerForTestCustomer_tg
|
||||
after insert on test_customer
|
||||
for each row
|
||||
execute procedure insertTriggerForTestCustomer_tf();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates INSERT INTO test_customer permissions for the related global rows.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
row global;
|
||||
begin
|
||||
call defineContext('create INSERT INTO test_customer permissions for the related global rows');
|
||||
|
||||
FOR row IN SELECT * FROM global
|
||||
LOOP
|
||||
call grantPermissionToRole(
|
||||
createPermission(row.uuid, 'INSERT', 'test_customer'),
|
||||
globalADMIN());
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Adds test_customer INSERT permission to specified role of new global rows.
|
||||
*/
|
||||
create or replace function test_customer_global_insert_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call grantPermissionToRole(
|
||||
createPermission(NEW.uuid, 'INSERT', 'test_customer'),
|
||||
globalADMIN());
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
|
||||
create trigger z_test_customer_global_insert_tg
|
||||
after insert on global
|
||||
for each row
|
||||
execute procedure test_customer_global_insert_tf();
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to test_customer,
|
||||
where only global-admin has that permission.
|
||||
*/
|
||||
create or replace function test_customer_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into test_customer not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger test_customer_insert_permission_check_tg
|
||||
before insert on test_customer
|
||||
for each row
|
||||
when ( not isGlobalAdmin() )
|
||||
execute procedure test_customer_insert_permission_missing_tf();
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
call generateRbacIdentityViewFromProjection('test_customer',
|
||||
$idName$
|
||||
prefix
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('test_customer',
|
||||
$orderBy$
|
||||
reference
|
||||
$orderBy$,
|
||||
$updates$
|
||||
reference = new.reference,
|
||||
prefix = new.prefix,
|
||||
adminUserName = new.adminUserName
|
||||
$updates$);
|
||||
--//
|
||||
|
@@ -28,17 +28,28 @@ declare
|
||||
currentTask varchar;
|
||||
custRowId uuid;
|
||||
custAdminName varchar;
|
||||
custAdminUuid uuid;
|
||||
newCust test_customer;
|
||||
begin
|
||||
currentTask = 'creating RBAC test customer #' || custReference || '/' || custPrefix;
|
||||
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global.admin');
|
||||
call defineContext(currentTask, null, 'superuser-alex@hostsharing.net', 'global#global:ADMIN');
|
||||
execute format('set local hsadminng.currentTask to %L', currentTask);
|
||||
|
||||
custRowId = uuid_generate_v4();
|
||||
custAdminName = 'customer-admin@' || custPrefix || '.example.com';
|
||||
custAdminUuid = createRbacUser(custAdminName);
|
||||
|
||||
insert
|
||||
into test_customer (reference, prefix, adminUserName)
|
||||
values (custReference, custPrefix, custAdminName);
|
||||
|
||||
select * into newCust
|
||||
from test_customer where reference=custReference;
|
||||
call grantRoleToUser(
|
||||
getRoleId(testCustomerOwner(newCust)),
|
||||
getRoleId(testCustomerAdmin(newCust)),
|
||||
custAdminUuid,
|
||||
true);
|
||||
end; $$;
|
||||
--//
|
||||
|
@@ -0,0 +1,59 @@
|
||||
### rbac package
|
||||
|
||||
This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually.
|
||||
|
||||
```mermaid
|
||||
%%{init:{'flowchart':{'htmlLabels':false}}}%%
|
||||
flowchart TB
|
||||
|
||||
subgraph package["`**package**`"]
|
||||
direction TB
|
||||
style package fill:#dd4901,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph package:roles[ ]
|
||||
style package:roles fill:#dd4901,stroke:white
|
||||
|
||||
role:package:OWNER[[package:OWNER]]
|
||||
role:package:ADMIN[[package:ADMIN]]
|
||||
role:package:TENANT[[package:TENANT]]
|
||||
end
|
||||
|
||||
subgraph package:permissions[ ]
|
||||
style package:permissions fill:#dd4901,stroke:white
|
||||
|
||||
perm:package:INSERT{{package:INSERT}}
|
||||
perm:package:DELETE{{package:DELETE}}
|
||||
perm:package:UPDATE{{package:UPDATE}}
|
||||
perm:package:SELECT{{package:SELECT}}
|
||||
end
|
||||
end
|
||||
|
||||
subgraph customer["`**customer**`"]
|
||||
direction TB
|
||||
style customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px
|
||||
|
||||
subgraph customer:roles[ ]
|
||||
style customer:roles fill:#99bcdb,stroke:white
|
||||
|
||||
role:customer:OWNER[[customer:OWNER]]
|
||||
role:customer:ADMIN[[customer:ADMIN]]
|
||||
role:customer:TENANT[[customer:TENANT]]
|
||||
end
|
||||
end
|
||||
|
||||
%% granting roles to roles
|
||||
role:global:ADMIN -.->|XX| role:customer:OWNER
|
||||
role:customer:OWNER -.-> role:customer:ADMIN
|
||||
role:customer:ADMIN -.-> role:customer:TENANT
|
||||
role:customer:ADMIN ==> role:package:OWNER
|
||||
role:package:OWNER ==> role:package:ADMIN
|
||||
role:package:ADMIN ==> role:package:TENANT
|
||||
role:package:TENANT ==> role:customer:TENANT
|
||||
|
||||
%% granting permissions to roles
|
||||
role:customer:ADMIN ==> perm:package:INSERT
|
||||
role:package:OWNER ==> perm:package:DELETE
|
||||
role:package:OWNER ==> perm:package:UPDATE
|
||||
role:package:TENANT ==> perm:package:SELECT
|
||||
|
||||
```
|
@@ -0,0 +1,230 @@
|
||||
--liquibase formatted sql
|
||||
-- This code generated was by RbacViewPostgresGenerator, do not amend manually.
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-OBJECT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRelatedRbacObject('test_package');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-ROLE-DESCRIPTORS:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRoleDescriptors('testPackage', 'test_package');
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-insert-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates the roles, grants and permission for the AFTER INSERT TRIGGER.
|
||||
*/
|
||||
|
||||
create or replace procedure buildRbacSystemForTestPackage(
|
||||
NEW test_package
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
newCustomer test_customer;
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM test_customer WHERE uuid = NEW.customerUuid INTO newCustomer;
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
|
||||
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testPackageOWNER(NEW),
|
||||
permissions => array['DELETE', 'UPDATE'],
|
||||
incomingSuperRoles => array[testCustomerADMIN(newCustomer)]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testPackageADMIN(NEW),
|
||||
incomingSuperRoles => array[testPackageOWNER(NEW)]
|
||||
);
|
||||
|
||||
perform createRoleWithGrants(
|
||||
testPackageTENANT(NEW),
|
||||
permissions => array['SELECT'],
|
||||
incomingSuperRoles => array[testPackageADMIN(NEW)],
|
||||
outgoingSubRoles => array[testCustomerTENANT(newCustomer)]
|
||||
);
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to create the role+grant structure for a new test_package row.
|
||||
*/
|
||||
|
||||
create or replace function insertTriggerForTestPackage_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call buildRbacSystemForTestPackage(NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger insertTriggerForTestPackage_tg
|
||||
after insert on test_package
|
||||
for each row
|
||||
execute procedure insertTriggerForTestPackage_tf();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-update-trigger:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Called from the AFTER UPDATE TRIGGER to re-wire the grants.
|
||||
*/
|
||||
|
||||
create or replace procedure updateRbacRulesForTestPackage(
|
||||
OLD test_package,
|
||||
NEW test_package
|
||||
)
|
||||
language plpgsql as $$
|
||||
|
||||
declare
|
||||
oldCustomer test_customer;
|
||||
newCustomer test_customer;
|
||||
|
||||
begin
|
||||
call enterTriggerForObjectUuid(NEW.uuid);
|
||||
|
||||
SELECT * FROM test_customer WHERE uuid = OLD.customerUuid INTO oldCustomer;
|
||||
assert oldCustomer.uuid is not null, format('oldCustomer must not be null for OLD.customerUuid = %s', OLD.customerUuid);
|
||||
|
||||
SELECT * FROM test_customer WHERE uuid = NEW.customerUuid INTO newCustomer;
|
||||
assert newCustomer.uuid is not null, format('newCustomer must not be null for NEW.customerUuid = %s', NEW.customerUuid);
|
||||
|
||||
|
||||
if NEW.customerUuid <> OLD.customerUuid then
|
||||
|
||||
call revokeRoleFromRole(testPackageOWNER(OLD), testCustomerADMIN(oldCustomer));
|
||||
call grantRoleToRole(testPackageOWNER(NEW), testCustomerADMIN(newCustomer));
|
||||
|
||||
call revokeRoleFromRole(testCustomerTENANT(oldCustomer), testPackageTENANT(OLD));
|
||||
call grantRoleToRole(testCustomerTENANT(newCustomer), testPackageTENANT(NEW));
|
||||
|
||||
end if;
|
||||
|
||||
call leaveTriggerForObjectUuid(NEW.uuid);
|
||||
end; $$;
|
||||
|
||||
/*
|
||||
AFTER INSERT TRIGGER to re-wire the grant structure for a new test_package row.
|
||||
*/
|
||||
|
||||
create or replace function updateTriggerForTestPackage_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call updateRbacRulesForTestPackage(OLD, NEW);
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
create trigger updateTriggerForTestPackage_tg
|
||||
after update on test_package
|
||||
for each row
|
||||
execute procedure updateTriggerForTestPackage_tf();
|
||||
--//
|
||||
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-INSERT:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Creates INSERT INTO test_package permissions for the related test_customer rows.
|
||||
*/
|
||||
do language plpgsql $$
|
||||
declare
|
||||
row test_customer;
|
||||
begin
|
||||
call defineContext('create INSERT INTO test_package permissions for the related test_customer rows');
|
||||
|
||||
FOR row IN SELECT * FROM test_customer
|
||||
LOOP
|
||||
call grantPermissionToRole(
|
||||
createPermission(row.uuid, 'INSERT', 'test_package'),
|
||||
testCustomerADMIN(row));
|
||||
END LOOP;
|
||||
END;
|
||||
$$;
|
||||
|
||||
/**
|
||||
Adds test_package INSERT permission to specified role of new test_customer rows.
|
||||
*/
|
||||
create or replace function test_package_test_customer_insert_tf()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
strict as $$
|
||||
begin
|
||||
call grantPermissionToRole(
|
||||
createPermission(NEW.uuid, 'INSERT', 'test_package'),
|
||||
testCustomerADMIN(NEW));
|
||||
return NEW;
|
||||
end; $$;
|
||||
|
||||
-- z_... is to put it at the end of after insert triggers, to make sure the roles exist
|
||||
create trigger z_test_package_test_customer_insert_tg
|
||||
after insert on test_customer
|
||||
for each row
|
||||
execute procedure test_package_test_customer_insert_tf();
|
||||
|
||||
/**
|
||||
Checks if the user or assumed roles are allowed to insert a row to test_package,
|
||||
where the check is performed by a direct role.
|
||||
|
||||
A direct role is a role depending on a foreign key directly available in the NEW row.
|
||||
*/
|
||||
create or replace function test_package_insert_permission_missing_tf()
|
||||
returns trigger
|
||||
language plpgsql as $$
|
||||
begin
|
||||
raise exception '[403] insert into test_package not allowed for current subjects % (%)',
|
||||
currentSubjects(), currentSubjectsUuids();
|
||||
end; $$;
|
||||
|
||||
create trigger test_package_insert_permission_check_tg
|
||||
before insert on test_package
|
||||
for each row
|
||||
when ( not hasInsertPermission(NEW.customerUuid, 'INSERT', 'test_package') )
|
||||
execute procedure test_package_insert_permission_missing_tf();
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
|
||||
call generateRbacIdentityViewFromProjection('test_package',
|
||||
$idName$
|
||||
name
|
||||
$idName$);
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
--changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--//
|
||||
-- ----------------------------------------------------------------------------
|
||||
call generateRbacRestrictedView('test_package',
|
||||
$orderBy$
|
||||
name
|
||||
$orderBy$,
|
||||
$updates$
|
||||
version = new.version,
|
||||
customerUuid = new.customerUuid,
|
||||
description = new.description
|
||||
$updates$);
|
||||
--//
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user