diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java index b010ed9b..3c9f0481 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorEntity.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.UUID; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Column.dependsOnColumn; +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.fetchedBySql; @@ -149,8 +150,8 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { SELECT * FROM hs_office_relation AS r WHERE r.type = 'DEBITOR' AND r.holderUuid = ${REF}.debitorRelUuid - """) - ) + """), + NULLABLE) .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) @@ -159,8 +160,8 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { SELECT * FROM hs_office_relation AS partnerRel WHERE ${debitorRel}.anchorUuid = partnerRel.holderUuid - """) - ) + """), + NULLABLE) .toRole("partnerRel", ADMIN).grantRole("debitorRel", ADMIN) .toRole("partnerRel", AGENT).grantRole("debitorRel", AGENT) .toRole("debitorRel", AGENT).grantRole("partnerRel", TENANT) diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java index 71e2b11a..0452c0df 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relation/HsOfficeRelationEntity.java @@ -16,6 +16,7 @@ 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.NULLABLE; 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.*; @@ -90,16 +91,16 @@ public class HsOfficeRelationEntity implements HasUuid, Stringifyable { .withUpdatableColumns("contactUuid") .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, dependsOnColumn("anchorUuid"), - fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.anchorUuid") - ) + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.anchorUuid"), + NULLABLE) .importEntityAlias("holderPerson", HsOfficePersonEntity.class, dependsOnColumn("holderUuid"), - fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.holderUuid") - ) + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.holderUuid"), + NULLABLE) .importEntityAlias("contact", HsOfficeContactEntity.class, dependsOnColumn("contactUuid"), - fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") - ) + fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid"), + NULLABLE) .createRole(OWNER, (with) -> { with.owningUser(CREATOR); with.incomingSuperRole(GLOBAL, ADMIN); diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java index 57499bb2..cf4e3d18 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -66,6 +66,21 @@ public class RbacView { private EntityAlias rootEntityAliasProxy; private RbacRoleDefinition previousRoleDef; + /** Crates an RBAC definition template for the given entity class and defining the given alias. + * + * @param alias + * an alias name for this entity/table, which can be used in further grants + * + * @param entityClass + * the Java class for which this RBAC definition is to be defined + * (the class to which the calling method belongs) + * + * @return + * the newly created RBAC definition template + * + * @param + * a JPA entity class extending RbacObject + */ public static RbacView rbacViewFor(final String alias, final Class entityClass) { return new RbacView(alias, entityClass); } @@ -77,22 +92,71 @@ public class RbacView { entityAliases.put("global", new EntityAlias("global")); } + /** + * Specifies, which columns of the restricted view are updatable at all. + * + * @param columnNames + * A list of the updatable columns. + * + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView withUpdatableColumns(final String... columnNames) { Collections.addAll(updatableColumns, columnNames); verifyVersionColumnExists(); return this; } + /** Specifies the SQL query which creates the identity view for this entity. + * + *

An identity view is a view which maps an objectUuid to an idName. + * The idName should be a human-readable representation of the row, but as short as possible. + * The idName must only consist of letters (A-Z, a-z), digits (0-9), dash (-), dot (.) and unserscore '_'. + * It's used to create the object-specific-role-names like test_customer#abc.admin - here 'abc' is the idName. + * The idName not necessarily unique in a table, but it should be avoided. + *

+ * + * @param sqlExpression + * Either specify an SQL projection (the part between SELECT and FROM), e.g. `SQL.projection("columnName") + * or the whole SELECT query returning the uuid and idName columns, + * e.g. `SQL.query("SELECT ... AS uuid, ... AS idName FROM ... JOIN ..."). + * Only add really important columns, just enough to create a short human-readable representation. + * + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView withIdentityView(final SQL sqlExpression) { this.identityViewSqlQuery = sqlExpression; return this; } + /** + * Specifies a ORDER BY clause for the generated restricted view. + * + *

A restricted view is generated, no matter if the order was specified or not.

+ * + * @param orderBySqlExpression + * That's the part behind `ORDER BY`, e.g. `SQL.expression("prefix"). + * + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) { this.orderBySqlExpression = orderBySqlExpression; return this; } + /** + * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table. + * + * @param role + * OWNER, ADMIN, AGENT etc. + * @param with + * a lambda which receives the created role to create grants and permissions to and from the newly created role, + * e.g. the owning user, incoming superroles, outgoing subroles + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView createRole(final Role role, final Consumer with) { final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); with.accept(newRoleDef); @@ -100,6 +164,15 @@ public class RbacView { return this; } + /** + * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table, + * which is becomes sub-role of the previously created role. + * + * @param role + * OWNER, ADMIN, AGENT etc. + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView createSubRole(final Role role) { final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); @@ -107,6 +180,19 @@ public class RbacView { return this; } + + /** + * Specifies that the given role (OWNER, ADMIN, ...) is to be created for new/updated roles in this table, + * which is becomes sub-role of the previously created role. + * + * @param role + * OWNER, ADMIN, AGENT etc. + * @param with + * a lambda which receives the created role to create grants and permissions to and from the newly created role, + * e.g. the owning user, incoming superroles, outgoing subroles + * @return + * the `this` instance itself to allow chained calls. + */ public RbacView createSubRole(final Role role, final Consumer with) { final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); @@ -115,10 +201,38 @@ public class RbacView { return this; } + /** + * Specifies that the given permission is to be created for each new row in the target table. + * + *

Grants to permissions created by this method have to be specified separately, + * often it's easier to read to use createRole/createSubRole and use with.permission(...).

+ * + * @param permission + * e.g. INSERT, SELECT, UPDATE, DELETE + * + * @return + * the newly created permission definition + */ public RbacPermissionDefinition createPermission(final Permission permission) { return createPermission(rootEntityAlias, permission); } + /** + * Specifies that the given permission is to be created for each new row in the target table, + * but for another table, e.g. a table with details data with different access rights. + * + *

Grants to permissions created by this method have to be specified separately, + * often it's easier to read to use createRole/createSubRole and use with.permission(...).

+ * + * @param entityAliasName + * A previously defined entity alias name. + * + * @param permission + * e.g. INSERT, SELECT, UPDATE, DELETE + * + * @return + * the newly created permission definition + */ public RbacPermissionDefinition createPermission(final String entityAliasName, final Permission permission) { return createPermission(findEntityAlias(entityAliasName), permission); } @@ -134,6 +248,32 @@ public class RbacView { return this; } + /** + * Imports the RBAC template from the given entity class and defines an alias name for it. + * This method is especially for proxy-entities, if the root entity does not have its own + * roles, a proxy-entity can be specified and its roles can be used instead. + * + * @param aliasName + * An alias name for the entity class. The same entity class can be imported multiple times, + * if multiple references to its table exist, then distinct alias names habe to be defined. + * + * @param entityClass + * A JPA entity class extending RbacObject which also implements an `rbac` method returning + * its RBAC specification. + * + * @param fetchSql + * An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the + * newly created or updated row (will be replaced by NEW/OLD from the trigger method). + * + * @param dependsOnColum + * The column, usually containing an uuid, on which this other table depends. + * + * @return + * the newly created permission definition + * + * @param + * a JPA entity class extending RbacObject + */ public RbacView importRootEntityAliasProxy( final String aliasName, final Class entityClass, @@ -146,6 +286,18 @@ public class RbacView { return this; } + /** + * Imports the RBAC template from the given entity class and defines an alias name for it. + * This method is especially to declare sub-entities, e.g. details to a main object. + * + * @see {@link} + * + * @return + * the newly created permission definition + * + * @param + * a JPA entity class extending RbacObject + */ public RbacView importSubEntityAlias( final String aliasName, final Class entityClass, final SQL fetchSql, final Column dependsOnColum) { @@ -153,6 +305,33 @@ public class RbacView { return this; } + /** + * Imports the RBAC template from the given entity class and defines an anlias name for it. + * + * @param aliasName + * An alias name for the entity class. The same entity class can be imported multiple times, + * if multiple references to its table exist, then distinct alias names habe to be defined. + * + * @param entityClass + * A JPA entity class extending RbacObject which also implements an `rbac` method returning + * its RBAC specification. + * + * @param fetchSql + * An SQL SELECT statement which fetches the referenced row. Use `${REF}` to speficiy the + * newly created or updated row (will be replaced by NEW/OLD from the trigger method). + * + * @param dependsOnColum + * The column, usually containing an uuid, on which this other table depends. + * + * @param nullable + * Specifies whether the dependsOnColum is nullable or not. + * + * @return + * the newly created permission definition + * + * @param + * a JPA entity class extending RbacObject + */ public RbacView importEntityAlias( final String aliasName, final Class entityClass, final Column dependsOnColum, final SQL fetchSql, final Nullable nullable) { @@ -160,13 +339,7 @@ public class RbacView { return this; } - public RbacView importEntityAlias( - final String aliasName, final Class entityClass, - final Column dependsOnColum, final SQL fetchSql) { - importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false, NOT_NULL); - return this; - } - + // TODO: remove once it's not used in HsOffice...Entity anymore public RbacView importEntityAlias( final String aliasName, final Class entityClass, final Column dependsOnColum) { @@ -232,6 +405,16 @@ public class RbacView { } } + /** + * Starts declaring a grant to a given role. + * + * @param entityAlias + * A previously speciried entity alias name. + * @param role + * OWNER, ADMIN, AGENT, ... + * @return + * a grant builder + */ public RbacGrantBuilder toRole(final String entityAlias, final Role role) { return new RbacGrantBuilder(entityAlias, role); } @@ -430,6 +613,16 @@ public class RbacView { permDefs.add(this); } + /** + * Grants the permission under definition to the given role. + * + * @param entityAlias + * A previously declared entity alias name. + * @param role + * OWNER, ADMIN, ... + * @return + * The RbacView specification to which this permission definition belongs. + */ public RbacView grantedTo(final String entityAlias, final Role role) { findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate(); return RbacView.this; @@ -460,19 +653,61 @@ public class RbacView { return this; } + /** + * Specifies which user becomes the owner of newly created objects. + * @param userRole + * GLOBAL_ADMIN, CREATOR, ... + * @return + * The grant definition for further chained calls. + */ public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) { return grantRoleToUser(this, findUserRef(userRole)); } + /** + * Specifies which permission is to be created for newly created objects. + * @param permission + * INSERT, SELECT, ... + * @return + * The grant definition for further chained calls. + */ public RbacGrantDefinition permission(final Permission permission) { return grantPermissionToRole(createPermission(entityAlias, permission), this); } + /** + * Specifies in incoming super role which gets granted the role under definition. + * + *

Incoming means an incoming grant arrow in our grant-diagrams. + * Super-role means that it's the role to which another role is granted. + * Both means actually the same, just in different aspects.

+ * + * @param entityAlias + * A previously declared entity alias name. + * @param role + * OWNER, ADMIN, ... + * @return + * The grant definition for further chained calls. + */ public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) { final var incomingSuperRole = findRbacRole(entityAlias, role); return grantSubRoleToSuperRole(this, incomingSuperRole); } + /** + * Specifies in outgoing sub role which gets granted the role under definition. + * + *

Outgoing means an outgoing grant arrow in our grant-diagrams. + * Sub-role means which is granted to another role. + * Both means actually the same, just in different aspects.

+ * + * @param entityAlias + * A previously declared entity alias name. + * @param role + * OWNER, ADMIN, ... + * @return + * The grant definition for further chained calls. + */ public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) { final var outgoingSubRole = findRbacRole(entityAlias, role); return grantSubRoleToSuperRole(outgoingSubRole, this); @@ -802,6 +1037,9 @@ public class RbacView { } } + /** + * This main method generates the RbacViews (PostgreSQL+diagram) for all given entity classes. + */ public static void main(String[] args) { Stream.of( TestCustomerEntity.class, diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java index 898a0273..d6a9bc28 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -4,7 +4,6 @@ import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; import java.nio.file.*; -import java.time.LocalDateTime; import static java.util.stream.Collectors.joining; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacGrantDefinition.GrantType.*; diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java index dc56a286..5a3b2be8 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -5,7 +5,6 @@ import lombok.SneakyThrows; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.time.LocalDateTime; import static net.hostsharing.hsadminng.rbac.rbacdef.PostgresTriggerReference.NEW; import static net.hostsharing.hsadminng.rbac.rbacdef.StringWriter.with; diff --git a/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java index fe053f1f..778c71ec 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java @@ -14,6 +14,7 @@ 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.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.fetchedBySql; @@ -52,7 +53,8 @@ public class TestDomainEntity implements HasUuid { fetchedBySql(""" SELECT * FROM test_package p WHERE p.uuid= ${ref}.packageUuid - """)) + """), + NULLABLE) .toRole("package", ADMIN).grantPermission(INSERT) .createRole(OWNER, (with) -> { diff --git a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java index 9dc0d5d9..27546cf2 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -14,6 +14,7 @@ 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.*; @@ -53,7 +54,8 @@ public class TestPackageEntity implements HasUuid { fetchedBySql(""" SELECT * FROM test_customer c WHERE c.uuid= ${ref}.customerUuid - """)) + """), + NOT_NULL) .toRole("customer", ADMIN).grantPermission(INSERT) .createRole(OWNER, (with) -> { diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 8de41891..0e5cc457 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -160,6 +160,7 @@ create or replace function cleanIdentifier(rawIdentifier varchar) declare cleanIdentifier varchar; begin + -- TODO: remove the ':' from the list of allowed characters as soon as it's not used anymore cleanIdentifier := regexp_replace(rawIdentifier, '[^A-Za-z0-9\-._:]+', '', 'g'); return cleanIdentifier; end; $$;