Compare commits

..

No commits in common. "84a25ac51d23bcff3d6df15e325ea70d5ee09b46" and "0f9b410d4fd4756a33ec65f4bb12316a9a41ca6f" have entirely different histories.

44 changed files with 238 additions and 140 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@
/build/www/** /build/www/**
/src/test/javascript/coverage/ /src/test/javascript/coverage/
/worktrees/ /worktrees/
TODO-progress.png
###################### ######################
# Node # Node

View File

@ -380,6 +380,12 @@ You can explore the prototype as follows:
`src/` `src/`
The actual source-code, see [Source Code Package Structure](#source-code-package-structure) for details. The actual source-code, see [Source Code Package Structure](#source-code-package-structure) for details.
`TODO.md`
Requirements of initial project. Do not touch!
`TODO-progress.png`
Generated diagram image of the project progress.
`tools/` `tools/`
Some shell-scripts to useful tasks. Some shell-scripts to useful tasks.
@ -759,4 +765,5 @@ The output will list the generated files.
## Further Documentation ## Further Documentation
- the `doc` directory contains architecture concepts and a glossary - the `doc` directory contains architecture concepts and a glossary
- the `ideas` directory contains unstructured ideas for future development or documentation - TODO.md tracks requirements and progress for the contract of the initial project,
please do not amend anything in this document

View File

@ -694,7 +694,7 @@ Users can view only the roles to which are granted to them.
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
and revoke grants to that role. and revoke grants to that role.
(TODO: access control part not yet implemented, currently all accessible roles can be granted to other users) (TODO: access control part not yet implemented)
Grants can be `managed`, which means they are created and deleted by system-defined rules. Grants can be `managed`, which means they are created and deleted by system-defined rules.
If a grant is not managed, it was created by an empowered user and can be deleted by empowered users. If a grant is not managed, it was created by an empowered user and can be deleted by empowered users.

View File

@ -87,7 +87,7 @@ Acceptance-Tests run on a fully integrated and deployed system with deployed dou
Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage. Acceptance-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
TODO.test: Complete the Acceptance-Tests test concept. TODO: Complete the Acceptance-Tests test concept.
#### Performance-Tests #### Performance-Tests
@ -107,4 +107,4 @@ We define System-Integration-Tests as test in which this system is deployed in a
System-Integration-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage. System-Integration-tests, are blackbox-tests and do <u>not</u> count into test-code-coverage.
TODO.test: Complete the System-Integration-Tests test concept. TODO: Complete the System-Integration-Tests test concept.

View File

@ -18,8 +18,8 @@ CREATE OR REPLACE FUNCTION historicize()
RETURNS trigger RETURNS trigger
LANGUAGE plpgsql STRICT AS $$ LANGUAGE plpgsql STRICT AS $$
DECLARE DECLARE
currentUser VARCHAR(63); currentUser VARCHAR(64);
currentTask VARCHAR(127); currentTask varchar;
"row" RECORD; "row" RECORD;
"alive" BOOLEAN; "alive" BOOLEAN;
"sql" varchar; "sql" varchar;
@ -37,27 +37,27 @@ END IF;
-- determine task -- determine task
currentTask = current_setting('hsadminng.currentTask'); currentTask = current_setting('hsadminng.currentTask');
assert currentTask IS NOT NULL AND length(currentTask) >= 12, IF (currentTask IS NULL OR length(currentTask) < 12) THEN
format('hsadminng.currentTask (%s) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask); RAISE EXCEPTION 'hsadminng.currentTask (%) must be defined and min 12 characters long, please use "SET LOCAL ...;"', currentTask;
assert length(currentTask) <= 127, END IF;
format('hsadminng.currentTask (%s) must not be longer than 127 characters"', currentTask); RAISE NOTICE 'currentTask: %', currentTask;
IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN IF (TG_OP = 'INSERT') OR (TG_OP = 'UPDATE') THEN
"row" := NEW; "row" := NEW;
"alive" := TRUE; "alive" := TRUE;
ELSE -- DELETE or TRUNCATE ELSE -- DELETE or TRUNCATE
"row" := OLD; "row" := OLD;
"alive" := FALSE; "alive" := FALSE;
END IF; END IF;
sql := format('INSERT INTO tx_history VALUES (txid_current(), now(), %1L, %2L) ON CONFLICT DO NOTHING', currentUser, currentTask); sql := format('INSERT INTO tx_history VALUES (txid_current(), now(), %1L, %2L) ON CONFLICT DO NOTHING', currentUser, currentTask);
RAISE NOTICE 'sql: %', sql; RAISE NOTICE 'sql: %', sql;
EXECUTE sql; EXECUTE sql;
sql := format('INSERT INTO %3$I_versions VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME); sql := format('INSERT INTO %3$I_versions VALUES (DEFAULT, txid_current(), %1$L, %2$L, $1.*)', TG_OP, alive, TG_TABLE_NAME);
RAISE NOTICE 'sql: %', sql; RAISE NOTICE 'sql: %', sql;
EXECUTE sql USING "row"; EXECUTE sql USING "row";
RETURN "row"; RETURN "row";
END; $$; END; $$;
CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar) CREATE OR REPLACE PROCEDURE create_historical_view(baseTable varchar)

View File

@ -55,15 +55,16 @@ public class Context {
final String currentRequest, final String currentRequest,
final String currentUser, final String currentUser,
final String assumedRoles) { final String assumedRoles) {
final var query = em.createNativeQuery(""" final var query = em.createNativeQuery(
call defineContext( """
cast(:currentTask as varchar(127)), call defineContext(
cast(:currentRequest as text), cast(:currentTask as varchar),
cast(:currentUser as varchar(63)), cast(:currentRequest as varchar),
cast(:assumedRoles as varchar(1023))); cast(:currentUser as varchar),
"""); cast(:assumedRoles as varchar));
query.setParameter("currentTask", shortenToMaxLength(currentTask, 127)); """);
query.setParameter("currentRequest", currentRequest); query.setParameter("currentTask", shortenToMaxLength(currentTask, 96));
query.setParameter("currentRequest", shortenToMaxLength(currentRequest, 512)); // TODO.spec: length?
query.setParameter("currentUser", currentUser); query.setParameter("currentUser", currentUser);
query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : ""); query.setParameter("assumedRoles", assumedRoles != null ? assumedRoles : "");
query.executeUpdate(); query.executeUpdate();

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.bankaccount;
import lombok.*; import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("BankAccount") @DisplayName("BankAccount")
public class HsOfficeBankAccountEntity implements RbacObject, Stringifyable { public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount") private static Stringify<HsOfficeBankAccountEntity> toString = stringify(HsOfficeBankAccountEntity.class, "bankAccount")
.withIdProp(HsOfficeBankAccountEntity::getIban) .withIdProp(HsOfficeBankAccountEntity::getIban)

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.contact;
import lombok.*; import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("Contact") @DisplayName("Contact")
public class HsOfficeContactEntity implements Stringifyable, RbacObject { public class HsOfficeContactEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact") private static Stringify<HsOfficeContactEntity> toString = stringify(HsOfficeContactEntity.class, "contact")
.withProp(Fields.label, HsOfficeContactEntity::getLabel) .withProp(Fields.label, HsOfficeContactEntity::getLabel)
@ -43,13 +43,13 @@ public class HsOfficeContactEntity implements Stringifyable, RbacObject {
private String label; private String label;
@Column(name = "postaladdress") @Column(name = "postaladdress")
private String postalAddress; // TODO.spec: check if we really want multiple, if so: JSON-Array or Postgres-Array? private String postalAddress; // TODO: check if we really want multiple, if so: JSON-Array or Postgres-Array?
@Column(name = "emailaddresses", columnDefinition = "json") @Column(name = "emailaddresses", columnDefinition = "json")
private String emailAddresses; // TODO.spec: check if we can really add multiple. format: ["eins@...", "zwei@..."] private String emailAddresses; // TODO: check if we can really add multiple. format: ["eins@...", "zwei@..."]
@Column(name = "phonenumbers", columnDefinition = "json") @Column(name = "phonenumbers", columnDefinition = "json")
private String phoneNumbers; // TODO.spec: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" } private String phoneNumbers; // TODO: check if we can really add multiple. format: { "office": "+49 40 12345-10", "fax": "+49 40 12345-05" }
@Override @Override
public String toString() { public String toString() {

View File

@ -4,7 +4,7 @@ package net.hostsharing.hsadminng.hs.office.coopassets;
import lombok.*; import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("CoopAssetsTransaction") @DisplayName("CoopAssetsTransaction")
public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, RbacObject { public class HsOfficeCoopAssetsTransactionEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class) private static Stringify<HsOfficeCoopAssetsTransactionEntity> stringify = stringify(HsOfficeCoopAssetsTransactionEntity.class)
.withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber) .withIdProp(HsOfficeCoopAssetsTransactionEntity::getTaggedMemberNumber)

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.coopshares;
import lombok.*; import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("CoopShareTransaction") @DisplayName("CoopShareTransaction")
public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, RbacObject { public class HsOfficeCoopSharesTransactionEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class) private static Stringify<HsOfficeCoopSharesTransactionEntity> stringify = stringify(HsOfficeCoopSharesTransactionEntity.class)
.withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged) .withProp(HsOfficeCoopSharesTransactionEntity::getMemberNumberTagged)

View File

@ -5,7 +5,7 @@ import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -16,7 +16,6 @@ import org.hibernate.annotations.NotFound;
import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.NotFoundAction;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Pattern;
import java.io.IOException; import java.io.IOException;
import java.util.UUID; import java.util.UUID;
@ -43,10 +42,9 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Debitor") @DisplayName("Debitor")
public class HsOfficeDebitorEntity implements RbacObject, Stringifyable { public class HsOfficeDebitorEntity implements HasUuid, Stringifyable {
public static final String DEBITOR_NUMBER_TAG = "D-"; public static final String DEBITOR_NUMBER_TAG = "D-";
public static final String TWO_DECIMAL_DIGITS = "^([0-9]{2})$";
private static Stringify<HsOfficeDebitorEntity> stringify = private static Stringify<HsOfficeDebitorEntity> stringify =
stringify(HsOfficeDebitorEntity.class, "debitor") stringify(HsOfficeDebitorEntity.class, "debitor")
@ -77,9 +75,8 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
@NotFound(action = NotFoundAction.IGNORE) @NotFound(action = NotFoundAction.IGNORE)
private HsOfficePartnerEntity partner; private HsOfficePartnerEntity partner;
@Column(name = "debitornumbersuffix", length = 2) @Column(name = "debitornumbersuffix", columnDefinition = "numeric(2)")
@Pattern(regexp = TWO_DECIMAL_DIGITS) private Byte debitorNumberSuffix; // TODO maybe rather as a formatted String?
private String debitorNumberSuffix;
@ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false) @ManyToOne(cascade = { PERSIST, MERGE, REFRESH, DETACH }, optional = false)
@JoinColumn(name = "debitorreluuid", nullable = false) @JoinColumn(name = "debitorreluuid", nullable = false)
@ -112,7 +109,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
.filter(partner -> debitorNumberSuffix != null) .filter(partner -> debitorNumberSuffix != null)
.map(HsOfficePartnerEntity::getPartnerNumber) .map(HsOfficePartnerEntity::getPartnerNumber)
.map(Object::toString) .map(Object::toString)
.map(partnerNumber -> partnerNumber + debitorNumberSuffix) .map(partnerNumber -> partnerNumber + String.format("%02d", debitorNumberSuffix))
.orElse(null); .orElse(null);
} }
@ -141,7 +138,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
JOIN hs_office_relation debitorRel JOIN hs_office_relation debitorRel
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR' ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
WHERE debitorRel.uuid = debitor.debitorRelUuid) WHERE debitorRel.uuid = debitor.debitorRelUuid)
|| debitorNumberSuffix as idName || to_char(debitorNumberSuffix, 'fm00') as idName
FROM hs_office_debitor AS debitor FROM hs_office_debitor AS debitor
""")) """))
.withRestrictedViewOrderBy(SQL.projection("defaultPrefix")) .withRestrictedViewOrderBy(SQL.projection("defaultPrefix"))
@ -153,7 +150,7 @@ public class HsOfficeDebitorEntity implements RbacObject, Stringifyable {
"vatCountryCode", "vatCountryCode",
"vatBusiness", "vatBusiness",
"vatReverseCharge", "vatReverseCharge",
"defaultPrefix" /* TODO.spec: do we want that updatable? */) "defaultPrefix" /* TODO: do we want that updatable? */)
.toRole("global", ADMIN).grantPermission(INSERT) .toRole("global", ADMIN).grantPermission(INSERT)
.importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class, .importRootEntityAliasProxy("debitorRel", HsOfficeRelationEntity.class,

View File

@ -5,7 +5,7 @@ import com.vladmihalcea.hibernate.type.range.Range;
import lombok.*; import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -14,7 +14,6 @@ import net.hostsharing.hsadminng.stringify.Stringifyable;
import org.hibernate.annotations.Type; import org.hibernate.annotations.Type;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Pattern;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.UUID; import java.util.UUID;
@ -42,10 +41,9 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Membership") @DisplayName("Membership")
public class HsOfficeMembershipEntity implements RbacObject, Stringifyable { public class HsOfficeMembershipEntity implements HasUuid, Stringifyable {
public static final String MEMBER_NUMBER_TAG = "M-"; 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) private static Stringify<HsOfficeMembershipEntity> stringify = stringify(HsOfficeMembershipEntity.class)
.withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber()) .withProp(e -> MEMBER_NUMBER_TAG + e.getMemberNumber())
@ -63,7 +61,6 @@ public class HsOfficeMembershipEntity implements RbacObject, Stringifyable {
private HsOfficePartnerEntity partner; private HsOfficePartnerEntity partner;
@Column(name = "membernumbersuffix", length = 2) @Column(name = "membernumbersuffix", length = 2)
@Pattern(regexp = TWO_DECIMAL_DIGITS)
private String memberNumberSuffix; private String memberNumberSuffix;
@Column(name = "validity", columnDefinition = "daterange") @Column(name = "validity", columnDefinition = "daterange")

View File

@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.office.partner;
import lombok.*; import lombok.*;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("PartnerDetails") @DisplayName("PartnerDetails")
public class HsOfficePartnerDetailsEntity implements RbacObject, Stringifyable { public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify( private static Stringify<HsOfficePartnerDetailsEntity> stringify = stringify(
HsOfficePartnerDetailsEntity.class, HsOfficePartnerDetailsEntity.class,

View File

@ -8,7 +8,7 @@ import lombok.Setter;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -45,7 +45,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("Partner") @DisplayName("Partner")
public class HsOfficePartnerEntity implements Stringifyable, RbacObject { public class HsOfficePartnerEntity implements Stringifyable, HasUuid {
public static final String PARTNER_NUMBER_TAG = "P-"; public static final String PARTNER_NUMBER_TAG = "P-";

View File

@ -11,7 +11,7 @@ public interface HsOfficePartnerRepository extends Repository<HsOfficePartnerEnt
Optional<HsOfficePartnerEntity> findByUuid(UUID id); Optional<HsOfficePartnerEntity> findByUuid(UUID id);
List<HsOfficePartnerEntity> findAll(); // TODO.impl: move to a repo in test sources List<HsOfficePartnerEntity> findAll(); // TODO: move to a repo in test sources
@Query(""" @Query("""
SELECT partner FROM HsOfficePartnerEntity partner SELECT partner FROM HsOfficePartnerEntity partner

View File

@ -3,7 +3,7 @@ package net.hostsharing.hsadminng.hs.office.person;
import lombok.*; import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -30,7 +30,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
@DisplayName("Person") @DisplayName("Person")
public class HsOfficePersonEntity implements RbacObject, Stringifyable { public class HsOfficePersonEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person") private static Stringify<HsOfficePersonEntity> toString = stringify(HsOfficePersonEntity.class, "person")
.withProp(Fields.personType, HsOfficePersonEntity::getPersonType) .withProp(Fields.personType, HsOfficePersonEntity::getPersonType)

View File

@ -4,7 +4,7 @@ import lombok.*;
import lombok.experimental.FieldNameConstants; import lombok.experimental.FieldNameConstants;
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
@ -32,7 +32,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@FieldNameConstants @FieldNameConstants
public class HsOfficeRelationEntity implements RbacObject, Stringifyable { public class HsOfficeRelationEntity implements HasUuid, Stringifyable {
private static Stringify<HsOfficeRelationEntity> toString = stringify(HsOfficeRelationEntity.class, "rel") private static Stringify<HsOfficeRelationEntity> toString = stringify(HsOfficeRelationEntity.class, "rel")
.withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor) .withProp(Fields.anchor, HsOfficeRelationEntity::getAnchor)

View File

@ -7,7 +7,7 @@ import net.hostsharing.hsadminng.errors.DisplayName;
import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity;
import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.stringify.Stringify; import net.hostsharing.hsadminng.stringify.Stringify;
import net.hostsharing.hsadminng.stringify.Stringifyable; import net.hostsharing.hsadminng.stringify.Stringifyable;
@ -37,7 +37,7 @@ import static net.hostsharing.hsadminng.stringify.Stringify.stringify;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@DisplayName("SEPA-Mandate") @DisplayName("SEPA-Mandate")
public class HsOfficeSepaMandateEntity implements Stringifyable, RbacObject { public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid {
private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class) private static Stringify<HsOfficeSepaMandateEntity> stringify = stringify(HsOfficeSepaMandateEntity.class)
.withProp(e -> e.getBankAccount().getIban()) .withProp(e -> e.getBankAccount().getIban())

View File

@ -0,0 +1,7 @@
package net.hostsharing.hsadminng.persistence;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
// TODO: remove this interface, I just wanted to avoid to many changes in that PR
public interface HasUuid extends RbacObject {
}

View File

@ -13,7 +13,7 @@ import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity;
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity;
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity; import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationEntity;
import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
import net.hostsharing.hsadminng.test.dom.TestDomainEntity; import net.hostsharing.hsadminng.test.dom.TestDomainEntity;
@ -277,7 +277,7 @@ public class RbacView {
*/ */
public <EC extends RbacObject> RbacView importRootEntityAliasProxy( public <EC extends RbacObject> RbacView importRootEntityAliasProxy(
final String aliasName, final String aliasName,
final Class<? extends RbacObject> entityClass, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final SQL fetchSql,
final Column dependsOnColum) { final Column dependsOnColum) {
if (rootEntityAliasProxy != null) { if (rootEntityAliasProxy != null) {
@ -300,7 +300,7 @@ public class RbacView {
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importSubEntityAlias( public RbacView importSubEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum) { final SQL fetchSql, final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL); importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true, NOT_NULL);
return this; return this;
@ -334,7 +334,7 @@ public class RbacView {
* a JPA entity class extending RbacObject * a JPA entity class extending RbacObject
*/ */
public RbacView importEntityAlias( public RbacView importEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) {
importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable); importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, nullable);
return this; return this;
@ -342,14 +342,14 @@ public class RbacView {
// TODO: remove once it's not used in HsOffice...Entity anymore // TODO: remove once it's not used in HsOffice...Entity anymore
public RbacView importEntityAlias( public RbacView importEntityAlias(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final Column dependsOnColum) { final Column dependsOnColum) {
importEntityAliasImpl(aliasName, entityClass, directlyFetchedByDependsOnColumn(), dependsOnColum, false, null); importEntityAliasImpl(aliasName, entityClass, directlyFetchedByDependsOnColumn(), dependsOnColum, false, null);
return this; return this;
} }
private EntityAlias importEntityAliasImpl( private EntityAlias importEntityAliasImpl(
final String aliasName, final Class<? extends RbacObject> entityClass, final String aliasName, final Class<? extends HasUuid> entityClass,
final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) { final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity, final Nullable nullable) {
final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable); final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity, nullable);
entityAliases.put(aliasName, entityAlias); entityAliases.put(aliasName, entityAlias);
@ -1046,7 +1046,7 @@ public class RbacView {
} }
} }
private static void generateRbacView(final Class<? extends RbacObject> c) { private static void generateRbacView(final Class<? extends HasUuid> c) {
final Method mainMethod = stream(c.getMethods()).filter( final Method mainMethod = stream(c.getMethods()).filter(
m -> isStatic(m.getModifiers()) && m.getName().equals("main") m -> isStatic(m.getModifiers()) && m.getName().equals("main")
) )

View File

@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestCustomerEntity implements RbacObject { public class TestCustomerEntity implements HasUuid {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.test.pac.TestPackageEntity; import net.hostsharing.hsadminng.test.pac.TestPackageEntity;
@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestDomainEntity implements RbacObject { public class TestDomainEntity implements HasUuid {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -4,7 +4,7 @@ import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView;
import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL; import net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL;
import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; import net.hostsharing.hsadminng.test.cust.TestCustomerEntity;
@ -26,7 +26,7 @@ import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@Setter @Setter
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class TestPackageEntity implements RbacObject { public class TestPackageEntity implements HasUuid {
@Id @Id
@GeneratedValue @GeneratedValue

View File

@ -23,7 +23,7 @@ components:
- ADMIN - ADMIN
- AGENT - AGENT
- TENANT - TENANT
- REFERRER
- GUEST - GUEST
- REFERRER
roleName: roleName:
type: string type: string

View File

@ -10,10 +10,10 @@
This function will be overwritten by later changesets. This function will be overwritten by later changesets.
*/ */
create procedure contextDefined( create procedure contextDefined(
currentTask varchar(127), currentTask varchar,
currentRequest text, currentRequest varchar,
currentUser varchar(63), currentUser varchar,
assumedRoles varchar(1023) assumedRoles varchar
) )
language plpgsql as $$ language plpgsql as $$
begin begin

View File

@ -248,7 +248,7 @@ declare
objectUuidOfRole uuid; objectUuidOfRole uuid;
roleUuid uuid; roleUuid uuid;
begin begin
-- TODO.refa: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences -- 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); objectTableFromRoleIdName = split_part(roleParts, '#', 1);
objectNameFromRoleIdName = split_part(roleParts, '#', 2); objectNameFromRoleIdName = split_part(roleParts, '#', 2);
@ -356,13 +356,16 @@ create trigger deleteRbacRolesOfRbacObject_Trigger
/* /*
*/ */
create domain RbacOp as varchar(6) create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone
check ( check (
VALUE = 'DELETE' VALUE = 'DELETE'
or VALUE = 'UPDATE' or VALUE = 'UPDATE'
or VALUE = 'SELECT' or VALUE = 'SELECT'
or VALUE = 'INSERT' or VALUE = 'INSERT'
or VALUE = 'ASSUME' 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 create table RbacPermission
@ -414,6 +417,37 @@ begin
return permissionUuid; return permissionUuid;
end; $$; end; $$;
-- TODO: deprecated, remove and amend all usages to createPermission
create or replace function createPermissions(forObjectUuid uuid, permitOps RbacOp[])
returns uuid[]
language plpgsql as $$
declare
refId uuid;
permissionIds uuid[] = array []::uuid[];
begin
if (forObjectUuid is null) then
raise exception 'forObjectUuid must not be null';
end if;
for i in array_lower(permitOps, 1)..array_upper(permitOps, 1)
loop
refId = (select uuid from RbacPermission where objectUuid = forObjectUuid and op = permitOps[i]);
if (refId is null) then
insert
into RbacReference ("type")
values ('RbacPermission')
returning uuid into refId;
insert
into RbacPermission (uuid, objectUuid, op)
values (refId, forObjectUuid, permitOps[i]);
end if;
permissionIds = permissionIds || refId;
end loop;
return permissionIds;
end;
$$;
create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
returns uuid returns uuid
returns null on null input returns null on null input
@ -615,6 +649,25 @@ begin
end; end;
$$; $$;
-- TODO: deprecated, remove and use grantPermissionToRole(...)
create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[])
language plpgsql as $$
begin
if cardinality(permissionIds) = 0 then return; end if;
for i in array_lower(permissionIds, 1)..array_upper(permissionIds, 1)
loop
perform assertReferenceType('roleId (ascendant)', roleUuid, 'RbacRole');
perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
insert
into RbacGrants (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
values (currentTriggerObjectUuid(), roleUuid, permissionIds[i], true)
on conflict do nothing; -- allow granting multiple times
end loop;
end;
$$;
create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true) create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true)
language plpgsql as $$ language plpgsql as $$
begin begin
@ -638,7 +691,7 @@ declare
superRoleId uuid; superRoleId uuid;
subRoleId uuid; subRoleId uuid;
begin begin
-- TODO.refa: maybe separate method grantRoleToRoleIfNotNull(...) for NULLABLE references -- TODO: maybe separate method grantRoleToRoleIfNotNull(...) for NULLABLE references
if superRole.objectUuid is null or subRole.objectuuid is null then if superRole.objectUuid is null or subRole.objectuuid is null then
return; return;
end if; end if;

View File

@ -85,10 +85,10 @@ end; $$;
This function will be overwritten by later changesets. This function will be overwritten by later changesets.
*/ */
create or replace procedure contextDefined( create or replace procedure contextDefined(
currentTask varchar(127), currentTask varchar,
currentRequest text, currentRequest varchar,
currentUser varchar(63), currentUser varchar,
assumedRoles varchar(1023) assumedRoles varchar
) )
language plpgsql as $$ language plpgsql as $$
declare declare

View File

@ -1,5 +1,18 @@
--liquibase formatted sql --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 ROLE -- CREATE ROLE
@ -19,8 +32,6 @@ create or replace function createRoleWithGrants(
language plpgsql as $$ language plpgsql as $$
declare declare
roleUuid uuid; roleUuid uuid;
permission RbacOp;
permissionUuid uuid;
subRoleDesc RbacRoleDescriptor; subRoleDesc RbacRoleDescriptor;
superRoleDesc RbacRoleDescriptor; superRoleDesc RbacRoleDescriptor;
subRoleUuid uuid; subRoleUuid uuid;
@ -30,11 +41,9 @@ declare
begin begin
roleUuid := createRole(roleDescriptor); roleUuid := createRole(roleDescriptor);
foreach permission in array permissions if cardinality(permissions) > 0 then
loop call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions));
permissionUuid := createPermission(roleDescriptor.objectuuid, permission); end if;
call grantPermissionToRole(permissionUuid, roleUuid);
end loop;
foreach superRoleDesc in array array_remove(incomingSuperRoles, null) foreach superRoleDesc in array array_remove(incomingSuperRoles, null)
loop loop
@ -51,7 +60,7 @@ begin
if cardinality(userUuids) > 0 then if cardinality(userUuids) > 0 then
-- direct grants to users need a grantedByRole which can revoke the grant -- direct grants to users need a grantedByRole which can revoke the grant
if grantedByRole is null then if grantedByRole is null then
userGrantsByRoleUuid := roleUuid; -- TODO.spec: or do we want to require an explicit userGrantsByRoleUuid? userGrantsByRoleUuid := roleUuid; -- TODO: or do we want to require an explicit userGrantsByRoleUuid?
else else
userGrantsByRoleUuid := getRoleId(grantedByRole); userGrantsByRoleUuid := getRoleId(grantedByRole);
end if; end if;

View File

@ -73,6 +73,15 @@ begin
return roleDescriptor('%2$s', entity.uuid, 'TENANT', assumed); return roleDescriptor('%2$s', entity.uuid, 'TENANT', assumed);
end; $f$; end; $f$;
-- 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', assumed);
end; $f$;
create or replace function %1$sReferrer(entity %2$s) create or replace function %1$sReferrer(entity %2$s)
returns RbacRoleDescriptor returns RbacRoleDescriptor
language plpgsql language plpgsql

View File

@ -7,7 +7,7 @@
create table hs_office_debitor create table hs_office_debitor
( (
uuid uuid unique references RbacObject (uuid) initially deferred, uuid uuid unique references RbacObject (uuid) initially deferred,
debitorNumberSuffix char(2) not null check (debitorNumberSuffix::text ~ '^[0-9][0-9]$'), debitorNumberSuffix numeric(2) not null,
debitorRelUuid uuid not null references hs_office_relation(uuid), debitorRelUuid uuid not null references hs_office_relation(uuid),
billable boolean not null default true, billable boolean not null default true,
vatId varchar(24), -- TODO.spec: here or in person? vatId varchar(24), -- TODO.spec: here or in person?

View File

@ -201,7 +201,7 @@ create trigger hs_office_debitor_insert_permission_check_tg
JOIN hs_office_relation debitorRel JOIN hs_office_relation debitorRel
ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR' ON debitorRel.anchorUuid = partnerRel.holderUuid AND debitorRel.type = 'DEBITOR'
WHERE debitorRel.uuid = debitor.debitorRelUuid) WHERE debitorRel.uuid = debitor.debitorRelUuid)
|| debitorNumberSuffix as idName || to_char(debitorNumberSuffix, 'fm00') as idName
FROM hs_office_debitor AS debitor FROM hs_office_debitor AS debitor
$idName$); $idName$);
--// --//

View File

@ -10,7 +10,7 @@
*/ */
create or replace procedure createHsOfficeSepaMandateTestData( create or replace procedure createHsOfficeSepaMandateTestData(
forPartnerNumber numeric(5), forPartnerNumber numeric(5),
forDebitorSuffix char(2), forDebitorSuffix numeric(2),
forIban varchar, forIban varchar,
withReference varchar) withReference varchar)
language plpgsql as $$ language plpgsql as $$
@ -48,9 +48,9 @@ end; $$;
do language plpgsql $$ do language plpgsql $$
begin begin
call createHsOfficeSepaMandateTestData(10001, '11', 'DE02120300000000202051', 'ref-10001-11'); call createHsOfficeSepaMandateTestData(10001, 11, 'DE02120300000000202051', 'ref-10001-11');
call createHsOfficeSepaMandateTestData(10002, '12', 'DE02100500000054540402', 'ref-10002-12'); call createHsOfficeSepaMandateTestData(10002, 12, 'DE02100500000054540402', 'ref-10002-12');
call createHsOfficeSepaMandateTestData(10003, '13', 'DE02300209000106531065', 'ref-10003-13'); call createHsOfficeSepaMandateTestData(10003, 13, 'DE02300209000106531065', 'ref-10003-13');
end; end;
$$; $$;
--// --//

View File

@ -12,7 +12,8 @@ create table if not exists hs_office_membership
( (
uuid uuid unique references RbacObject (uuid) initially deferred, uuid uuid unique references RbacObject (uuid) initially deferred,
partnerUuid uuid not null references hs_office_partner(uuid), partnerUuid uuid not null references hs_office_partner(uuid),
memberNumberSuffix char(2) not null check (memberNumberSuffix::text ~ '^[0-9][0-9]$'), memberNumberSuffix char(2) not null check (
memberNumberSuffix::text ~ '^[0-9][0-9]$'),
validity daterange not null, validity daterange not null,
reasonForTermination HsOfficeReasonForTermination not null default 'NONE', reasonForTermination HsOfficeReasonForTermination not null default 'NONE',
membershipFeeBillable boolean not null default true, membershipFeeBillable boolean not null default true,

View File

@ -121,7 +121,7 @@ public class ArchitectureTest {
.should().onlyBeAccessed().byClassesThat() .should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage( .resideInAnyPackage(
"..hs.office.(*)..", "..hs.office.(*)..",
"..rbac.rbacgrant" // TODO.test: just because of RbacGrantsDiagramServiceIntegrationTest "..rbac.rbacgrant" // TODO: just because of RbacGrantsDiagramServiceIntegrationTest
); );
@ArchTest @ArchTest

View File

@ -27,12 +27,12 @@ import static org.mockito.Mockito.verify;
class ContextUnitTest { class ContextUnitTest {
private static final String DEFINE_CONTEXT_QUERY_STRING = """ private static final String DEFINE_CONTEXT_QUERY_STRING = """
call defineContext( call defineContext(
cast(:currentTask as varchar(127)), cast(:currentTask as varchar),
cast(:currentRequest as text), cast(:currentRequest as varchar),
cast(:currentUser as varchar(63)), cast(:currentUser as varchar),
cast(:assumedRoles as varchar(1023))); cast(:assumedRoles as varchar));
"""; """;
@Nested @Nested
class WithoutHttpRequest { class WithoutHttpRequest {
@ -71,7 +71,7 @@ class ContextUnitTest {
context.define("current-user"); context.define("current-user");
verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING); verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING);
verify(nativeQuery).setParameter("currentRequest", null); verify(nativeQuery).setParameter("currentRequest", "");
} }
} }
@ -142,8 +142,8 @@ class ContextUnitTest {
} }
@Test @Test
void shortensCurrentTaskToMaxLength() throws IOException { void shortensCurrentTaskTo96Chars() throws IOException {
givenRequest("GET", "http://localhost:9999/api/endpoint/" + "0123456789".repeat(13), givenRequest("GET", "http://localhost:9999/api/endpoint/" + "0123456789".repeat(10),
Map.ofEntries( Map.ofEntries(
Map.entry("current-user", "given-user"), Map.entry("current-user", "given-user"),
Map.entry("content-type", "application/json"), Map.entry("content-type", "application/json"),
@ -153,7 +153,26 @@ class ContextUnitTest {
context.define("current-user"); context.define("current-user");
verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING); verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING);
verify(nativeQuery).setParameter(eq("currentTask"), argThat((String t) -> t.length() == 127)); verify(nativeQuery).setParameter(eq("currentTask"), argThat((String t) -> t.length() == 96));
}
@Test
void shortensCurrentRequestTo512Chars() throws IOException {
givenRequest("GET", "http://localhost:9999/api/endpoint",
Map.ofEntries(
Map.entry("current-user", "given-user"),
Map.entry("content-type", "application/json"),
Map.entry("user-agent", "given-user-agent")),
"""
{
"dummy": "%s"
}
""".formatted("0123456789".repeat(60)));
context.define("current-user");
verify(em).createNativeQuery(DEFINE_CONTEXT_QUERY_STRING);
verify(nativeQuery).setParameter(eq("currentRequest"), argThat((String t) -> t.length() == 512));
} }
private void givenRequest(final String method, final String url, final Map<String, String> headers, final String body) private void givenRequest(final String method, final String url, final Map<String, String> headers, final String body)

View File

@ -722,7 +722,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0); final var givenPartner = partnerRepo.findPartnerByOptionalNameLike("Fourth").get(0);
final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0); final var givenContact = contactRepo.findContactByOptionalLabelLike("fourth contact").get(0);
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix(nextDebitorSuffix()) .debitorNumberSuffix(++nextDebitorSuffix)
.billable(true) .billable(true)
.debitorRel( .debitorRel(
HsOfficeRelationEntity.builder() HsOfficeRelationEntity.builder()
@ -751,8 +751,4 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu
System.out.printf("deleted %d entities%n", count); System.out.printf("deleted %d entities%n", count);
}); });
} }
private String nextDebitorSuffix() {
return String.format("%02d", nextDebitorSuffix++);
}
} }

View File

@ -26,7 +26,7 @@ class HsOfficeDebitorEntityUnitTest {
@Test @Test
void toStringContainsPartnerAndContact() { void toStringContainsPartnerAndContact() {
final var given = HsOfficeDebitorEntity.builder() final var given = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("67") .debitorNumberSuffix((byte)67)
.debitorRel(givenDebitorRel) .debitorRel(givenDebitorRel)
.defaultPrefix("som") .defaultPrefix("som")
.partner(HsOfficePartnerEntity.builder() .partner(HsOfficePartnerEntity.builder()
@ -43,7 +43,7 @@ class HsOfficeDebitorEntityUnitTest {
void toShortStringContainsDebitorNumber() { void toShortStringContainsDebitorNumber() {
final var given = HsOfficeDebitorEntity.builder() final var given = HsOfficeDebitorEntity.builder()
.debitorRel(givenDebitorRel) .debitorRel(givenDebitorRel)
.debitorNumberSuffix("67") .debitorNumberSuffix((byte)67)
.partner(HsOfficePartnerEntity.builder() .partner(HsOfficePartnerEntity.builder()
.partnerNumber(12345) .partnerNumber(12345)
.build()) .build())
@ -58,7 +58,7 @@ class HsOfficeDebitorEntityUnitTest {
void getDebitorNumberWithPartnerNumberAndDebitorNumberSuffix() { void getDebitorNumberWithPartnerNumberAndDebitorNumberSuffix() {
final var given = HsOfficeDebitorEntity.builder() final var given = HsOfficeDebitorEntity.builder()
.debitorRel(givenDebitorRel) .debitorRel(givenDebitorRel)
.debitorNumberSuffix("67") .debitorNumberSuffix((byte)67)
.partner(HsOfficePartnerEntity.builder() .partner(HsOfficePartnerEntity.builder()
.partnerNumber(12345) .partnerNumber(12345)
.build()) .build())
@ -73,7 +73,7 @@ class HsOfficeDebitorEntityUnitTest {
void getDebitorNumberWithoutPartnerReturnsNull() { void getDebitorNumberWithoutPartnerReturnsNull() {
final var given = HsOfficeDebitorEntity.builder() final var given = HsOfficeDebitorEntity.builder()
.debitorRel(givenDebitorRel) .debitorRel(givenDebitorRel)
.debitorNumberSuffix("67") .debitorNumberSuffix((byte)67)
.partner(null) .partner(null)
.build(); .build();
@ -86,7 +86,7 @@ class HsOfficeDebitorEntityUnitTest {
void getDebitorNumberWithoutPartnerNumberReturnsNull() { void getDebitorNumberWithoutPartnerNumberReturnsNull() {
final var given = HsOfficeDebitorEntity.builder() final var given = HsOfficeDebitorEntity.builder()
.debitorRel(givenDebitorRel) .debitorRel(givenDebitorRel)
.debitorNumberSuffix("67") .debitorNumberSuffix((byte)67)
.partner(HsOfficePartnerEntity.builder().build()) .partner(HsOfficePartnerEntity.builder().build())
.build(); .build();

View File

@ -89,7 +89,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// when // when
final var result = attempt(em, () -> { final var result = attempt(em, () -> {
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("21") .debitorNumberSuffix((byte)21)
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
.anchor(givenPartnerPerson) .anchor(givenPartnerPerson)
@ -121,7 +121,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
// when // when
final var result = attempt(em, () -> { final var result = attempt(em, () -> {
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("21") .debitorNumberSuffix((byte)21)
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
.anchor(givenPartnerPerson) .anchor(givenPartnerPerson)
@ -156,7 +156,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final var givenDebitorPerson = one(personRepo.findPersonByOptionalNameLike("Fourth eG")); final var givenDebitorPerson = one(personRepo.findPersonByOptionalNameLike("Fourth eG"));
final var givenContact = one(contactRepo.findContactByOptionalLabelLike("fourth contact")); final var givenContact = one(contactRepo.findContactByOptionalLabelLike("fourth contact"));
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("22") .debitorNumberSuffix((byte)22)
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
.anchor(givenPartnerPerson) .anchor(givenPartnerPerson)
@ -613,7 +613,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
final var givenBankAccount = final var givenBankAccount =
bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null; bankAccountHolder != null ? one(bankAccountRepo.findByOptionalHolderLike(bankAccountHolder)) : null;
final var newDebitor = HsOfficeDebitorEntity.builder() final var newDebitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("20") .debitorNumberSuffix((byte)20)
.debitorRel(HsOfficeRelationEntity.builder() .debitorRel(HsOfficeRelationEntity.builder()
.type(HsOfficeRelationType.DEBITOR) .type(HsOfficeRelationType.DEBITOR)
.anchor(givenPartnerPerson) .anchor(givenPartnerPerson)

View File

@ -10,7 +10,7 @@ import static net.hostsharing.hsadminng.hs.office.partner.TestHsOfficePartner.TE
@UtilityClass @UtilityClass
public class TestHsOfficeDebitor { public class TestHsOfficeDebitor {
public String DEFAULT_DEBITOR_SUFFIX = "00"; public byte DEFAULT_DEBITOR_SUFFIX = 0;
public static final HsOfficeDebitorEntity TEST_DEBITOR = HsOfficeDebitorEntity.builder() public static final HsOfficeDebitorEntity TEST_DEBITOR = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix(DEFAULT_DEBITOR_SUFFIX) .debitorNumberSuffix(DEFAULT_DEBITOR_SUFFIX)

View File

@ -724,7 +724,7 @@ public class ImportOfficeData extends ContextBasedTest {
relations.put(relationId++, debitorRel); relations.put(relationId++, debitorRel);
final var debitor = HsOfficeDebitorEntity.builder() final var debitor = HsOfficeDebitorEntity.builder()
.debitorNumberSuffix("00") .debitorNumberSuffix((byte) 0)
.partner(partner) .partner(partner)
.debitorRel(debitorRel) .debitorRel(debitorRel)
.defaultPrefix(rec.getString("member_code").replace("hsh00-", "")) .defaultPrefix(rec.getString("member_code").replace("hsh00-", ""))

View File

@ -1,7 +1,7 @@
package net.hostsharing.hsadminng.hs.office.test; package net.hostsharing.hsadminng.hs.office.test;
import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.hsadminng.context.ContextBasedTest;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository;
import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService;
@ -66,7 +66,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
return merged; return merged;
} }
public UUID toCleanup(final Class<? extends RbacObject> entityClass, final UUID uuidToCleanup) { public UUID toCleanup(final Class<? extends HasUuid> entityClass, final UUID uuidToCleanup) {
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup); out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup);
entitiesToCleanup.put(uuidToCleanup, entityClass); entitiesToCleanup.put(uuidToCleanup, entityClass);
return uuidToCleanup; return uuidToCleanup;
@ -81,7 +81,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
return entity; return entity;
} }
protected void cleanupAllNew(final Class<? extends RbacObject> entityClass) { protected void cleanupAllNew(final Class<? extends HasUuid> entityClass) {
if (initialRbacObjects == null) { if (initialRbacObjects == null) {
out.println("skipping cleanupAllNew: " + entityClass.getSimpleName()); out.println("skipping cleanupAllNew: " + entityClass.getSimpleName());
return; // TODO: seems @AfterEach is called without any @BeforeEach return; // TODO: seems @AfterEach is called without any @BeforeEach

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.hs.office.test; package net.hostsharing.hsadminng.hs.office.test;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import java.util.List; import java.util.List;
@ -8,7 +8,7 @@ import static org.assertj.core.api.Assertions.assertThat;
public class EntityList { public class EntityList {
public static <E extends RbacObject> E one(final List<E> entities) { public static <E extends HasUuid> E one(final List<E> entities) {
assertThat(entities).hasSize(1); assertThat(entities).hasSize(1);
return entities.stream().findFirst().orElseThrow(); return entities.stream().findFirst().orElseThrow();
} }

View File

@ -130,6 +130,7 @@ public class JpaAttempt {
final Class<? extends RuntimeException> expectedExceptionClass, final Class<? extends RuntimeException> expectedExceptionClass,
final String... expectedRootCauseMessages) { final String... expectedRootCauseMessages) {
assertThat(wasSuccessful()).as("wasSuccessful").isFalse(); assertThat(wasSuccessful()).as("wasSuccessful").isFalse();
// TODO: also check the expected exception class itself
final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass)); final String firstRootCauseMessageLine = firstRootCauseMessageLineOf(caughtException(expectedExceptionClass));
for (String expectedRootCauseMessage : expectedRootCauseMessages) { for (String expectedRootCauseMessage : expectedRootCauseMessages) {
assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage); assertThat(firstRootCauseMessageLine).contains(expectedRootCauseMessage);

View File

@ -1,6 +1,6 @@
package net.hostsharing.test; package net.hostsharing.test;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.EntityPatcher;
import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -233,7 +233,7 @@ public abstract class PatchUnitTestBase<R, E> {
} }
} }
protected static class JsonNullableProperty<R, RV, E extends RbacObject, EV> extends Property<R, RV, E, EV> { protected static class JsonNullableProperty<R, RV, E extends HasUuid, EV> extends Property<R, RV, E, EV> {
private final BiConsumer<R, JsonNullable<RV>> resourceSetter; private final BiConsumer<R, JsonNullable<RV>> resourceSetter;
public final RV givenPatchValue; public final RV givenPatchValue;