diff --git a/doc/rbac.md b/doc/rbac.md index 06a6ee7e..9aa4b024 100644 --- a/doc/rbac.md +++ b/doc/rbac.md @@ -11,7 +11,7 @@ Our implementation is based on Role-Based-Access-Management (RBAC) in conjunctio As far as possible, we are using the same terms as defined in the RBAC standard, for our function names though, we chose more expressive names. In RBAC, subjects can be assigned to roles, roles can be hierarchical and eventually have assigned permissions. -A permission allows a specific operation (e.g. view or edit) on a specific (business-) object. +A permission allows a specific operation (e.g. SELECT or UPDATE) on a specific (business-) object. You can find the entity structure as a UML class diagram as follows: @@ -101,13 +101,12 @@ package RBAC { RbacPermission *-- RbacObject enum RbacOperation { - add-package - add-domain - add-domain + INSERT:package + INSERT:domain ... - view - edit - delete + SELECT + UPDATE + DELETE } entity RbacObject { @@ -172,11 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject* An *RbacOperation* determines, what an *RbacPermission* allows to do. It can be one of: -- **'add-...'** - permits creating new instances of specific entity types underneath the object specified by the permission, e.g. "add-package" -- **'view'** - permits reading the contents of the object specified by the permission -- **'edit'** - change the contents of the object specified by the permission -- **'delete'** - delete the object specified by the permission -- **'\*'** +- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column, includes 'SELECT' +- **'SELECT'** - permits selecting the row specified by the permission, is included in all other permissions +- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission, includes 'SELECT' +- **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT' This list is extensible according to the needs of the access rule system. @@ -212,7 +210,7 @@ E.g. for a new *customer* it would be granted to 'administrators' and for a new Whoever has the owner-role assigned can do everything with the related business-object, including deleting (or deactivating) it. -In most cases, the permissions to other operations than 'delete' are granted through the 'admin' role. +In most cases, the permissions to other operations than 'DELETE' are granted through the 'admin' role. By this, all roles ob sub-objects, which are assigned to the 'admin' role, are also granted to the 'owner'. #### admin @@ -220,14 +218,14 @@ By this, all roles ob sub-objects, which are assigned to the 'admin' role, are a The admin-role is granted to a role of those subjects who manage the business object. E.g. a 'package' is manged by the admin of the customer. -Whoever has the admin-role assigned, can usually edit the related business-object but not deleting (or deactivating) it. +Whoever has the admin-role assigned, can usually update the related business-object but not delete (or deactivating) it. -The admin-role also comprises lesser roles, through which the view-permission is granted. +The admin-role also comprises lesser roles, through which the SELECT-permission is granted. #### agent The agent-role is not used in the examples of this document, because it's for more complex cases. -It's usually granted to those roles and users who represent the related business-object, but are not allowed to edit it. +It's usually granted to those roles and users who represent the related business-object, but are not allowed to update it. Other than the tenant-role, it usually offers broader visibility of sub-business-objects (joined entities). E.g. a package-admin is allowed to see the related debitor-business-object, @@ -235,19 +233,19 @@ but not its banking data. #### tenant -The tenant-role is granted to everybody who needs to be able to view the business-object and (probably some) related business-objects. +The tenant-role is granted to everybody who needs to be able to select the business-object and (probably some) related business-objects. Usually all owners, admins and tenants of sub-objects get this role granted. -Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get view permission. +Some business-objects only have very limited data directly in the main business-object and store more sensitive data in special sub-objects (e.g. 'customer-details') to which tenants of sub-objects of the main-object (e.g. package admins) do not get SELECT permission. #### guest Like the agent-role, the guest-role too is not used in the examples of this document, because it's for more complex cases. -If the guest-role exists, the view-permission is granted to it, instead of to the tenant-role. +If the guest-role exists, the SELECT-permission is granted to it, instead of to the tenant-role. Other than the tenant-role, the guest-roles does never grant any roles of related objects. -Also, if the guest-role exists, the tenant-role receives the view-permission through the guest-role. +Also, if the guest-role exists, the tenant-role receives the SELECT-permission through the guest-role. ### Referenced Business Objects and Role-Depreciation @@ -263,7 +261,7 @@ The admin-role of one object could be granted visibility to another object throu But not in all cases role-depreciation takes place. E.g. often a tenant-role is granted another tenant-role, -because it should be again allowed to view sub-objects. +because it should be again allowed to select sub-objects. The same for the agent-role, often it is granted another agent-role. @@ -297,14 +295,14 @@ package RbacRoles { RbacUsers -[hidden]> RbacRoles package RbacPermissions { - object PermCustXyz_View - object PermCustXyz_Edit - object PermCustXyz_Delete - object PermCustXyz_AddPackage - object PermPackXyz00_View - object PermPackXyz00_Edit - object PermPackXyz00_Delete - object PermPackXyz00_AddUser + object PermCustXyz_SELECT + object PermCustXyz_UPDATE + object PermCustXyz_DELETE + object PermCustXyz_INSERT:Package + object PermPackXyz00_SELECT + object PermPackXyz00_EDIT + object PermPackXyz00_DELETE + object PermPackXyz00_INSERT:USER } RbacRoles -[hidden]> RbacPermissions @@ -322,23 +320,23 @@ RoleAdministrators o..> RoleCustXyz_Owner RoleCustXyz_Owner o-> RoleCustXyz_Admin RoleCustXyz_Admin o-> RolePackXyz00_Owner -RoleCustXyz_Owner o--> PermCustXyz_Edit -RoleCustXyz_Owner o--> PermCustXyz_Delete -RoleCustXyz_Admin o--> PermCustXyz_View -RoleCustXyz_Admin o--> PermCustXyz_AddPackage -RolePackXyz00_Owner o--> PermPackXyz00_View -RolePackXyz00_Owner o--> PermPackXyz00_Edit -RolePackXyz00_Owner o--> PermPackXyz00_Delete -RolePackXyz00_Owner o--> PermPackXyz00_AddUser +RoleCustXyz_Owner o--> PermCustXyz_UPDATE +RoleCustXyz_Owner o--> PermCustXyz_DELETE +RoleCustXyz_Admin o--> PermCustXyz_SELECT +RoleCustXyz_Admin o--> PermCustXyz_INSERT:Package +RolePackXyz00_Owner o--> PermPackXyz00_SELECT +RolePackXyz00_Owner o--> PermPackXyz00_UPDATE +RolePackXyz00_Owner o--> PermPackXyz00_DELETE +RolePackXyz00_Owner o--> PermPackXyz00_INSERT:User -PermCustXyz_View o--> CustXyz -PermCustXyz_Edit o--> CustXyz -PermCustXyz_Delete o--> CustXyz -PermCustXyz_AddPackage o--> CustXyz -PermPackXyz00_View o--> PackXyz00 -PermPackXyz00_Edit o--> PackXyz00 -PermPackXyz00_Delete o--> PackXyz00 -PermPackXyz00_AddUser o--> PackXyz00 +PermCustXyz_SELECT o--> CustXyz +PermCustXyz_UPDATE o--> CustXyz +PermCustXyz_DELETE o--> CustXyz +PermCustXyz_INSERT:Package o--> CustXyz +PermPackXyz00_SELECT o--> PackXyz00 +PermPackXyz00_UPDATE o--> PackXyz00 +PermPackXyz00_DELETE o--> PackXyz00 +PermPackXyz00_INSERT:User o--> PackXyz00 @enduml ``` @@ -353,12 +351,12 @@ To support the RBAC system, for each business-object-table, some more artifacts Not yet implemented, but planned are these actions: -- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'delete' permission, -- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'edit' right, -- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has 'add-..' right to the parent-business-object. +- an `ON DELETE ... DO INSTEAD` rule to allow `SQL DELETE` if applicable for the business-object-table and the user has 'DELETE' permission, +- an `ON UPDATE ... DO INSTEAD` rule to allow `SQL UPDATE` if the user has 'UPDATE' right, +- an `ON INSERT ... DO INSTEAD` rule to allow `SQL INSERT` if the user has the 'INSERT' right for the parent-business-object. The restricted view takes the current user from a session property and applies the hierarchy of its roles all the way down to the permissions related to the respective business-object-table. -This way, each user can only view the data they have 'view'-permission for, only create those they have 'add-...'-permission, only update those they have 'edit'- and only delete those they have 'delete'-permission to. +This way, each user can only select the data they have 'SELECT'-permission for, only create those they have 'add-...'-permission, only update those they have 'UPDATE'- and only delete those they have 'DELETE'-permission to. ### Current User @@ -458,26 +456,26 @@ allow_mixing entity "BObj customer#xyz" as boCustXyz together { - entity "Perm customer#xyz *" as permCustomerXyzAll - permCustomerXyzAll --> boCustXyz + entity "Perm customer#xyz *" as permCustomerXyzDELETE + permCustomerXyzDELETE --> boCustXyz - entity "Perm customer#xyz add-package" as permCustomerXyzAddPack - permCustomerXyzAddPack --> boCustXyz + entity "Perm customer#xyz INSERT:package" as permCustomerXyzINSERT:package + permCustomerXyzINSERT:package --> boCustXyz - entity "Perm customer#xyz view" as permCustomerXyzView - permCustomerXyzView --> boCustXyz + entity "Perm customer#xyz SELECT" as permCustomerXyzSELECT + permCustomerXyzSELECT--> boCustXyz } entity "Role customer#xyz.tenant" as roleCustXyzTenant -roleCustXyzTenant --> permCustomerXyzView +roleCustXyzTenant --> permCustomerXyzSELECT entity "Role customer#xyz.admin" as roleCustXyzAdmin roleCustXyzAdmin --> roleCustXyzTenant -roleCustXyzAdmin --> permCustomerXyzAddPack +roleCustXyzAdmin --> permCustomerXyzINSERT:package entity "Role customer#xyz.owner" as roleCustXyzOwner roleCustXyzOwner ..> roleCustXyzAdmin -roleCustXyzOwner --> permCustomerXyzAll +roleCustXyzOwner --> permCustomerXyzDELETE actor "Customer XYZ Admin" as actorCustXyzAdmin actorCustXyzAdmin --> roleCustXyzAdmin @@ -487,8 +485,6 @@ roleAdmins --> roleCustXyzOwner actor "Any Hostmaster" as actorHostmaster actorHostmaster --> roleAdmins - - @enduml ``` @@ -527,17 +523,17 @@ allow_mixing entity "BObj package#xyz00" as boPacXyz00 together { - entity "Perm package#xyz00 *" as permPackageXyzAll - permPackageXyzAll --> boPacXyz00 + entity "Perm package#xyz00 *" as permPackageXyzDELETE + permPackageXyzDELETE --> boPacXyz00 - entity "Perm package#xyz00 add-domain" as permPacXyz00AddUser - permPacXyz00AddUser --> boPacXyz00 + entity "Perm package#xyz00 INSERT:domain" as permPacXyz00INSERT:user + permPacXyz00INSERT:user --> boPacXyz00 - entity "Perm package#xyz00 edit" as permPacXyz00Edit - permPacXyz00Edit --> boPacXyz00 + entity "Perm package#xyz00 UPDATE" as permPacXyz00UPDATE + permPacXyz00UPDATE --> boPacXyz00 - entity "Perm package#xyz00 view" as permPacXyz00View - permPacXyz00View --> boPacXyz00 + entity "Perm package#xyz00 SELECT" as permPacXyz00SELECT + permPacXyz00SELECT --> boPacXyz00 } package { @@ -552,11 +548,11 @@ package { entity "Role package#xyz00.tenant" as rolePacXyz00Tenant } -rolePacXyz00Tenant --> permPacXyz00View +rolePacXyz00Tenant --> permPacXyz00SELECT rolePacXyz00Tenant --> roleCustXyzTenant rolePacXyz00Owner --> rolePacXyz00Admin -rolePacXyz00Owner --> permPackageXyzAll +rolePacXyz00Owner --> permPackageXyzDELETE roleCustXyzAdmin --> rolePacXyz00Owner roleCustXyzAdmin --> roleCustXyzTenant @@ -564,8 +560,8 @@ roleCustXyzAdmin --> roleCustXyzTenant roleCustXyzOwner ..> roleCustXyzAdmin rolePacXyz00Admin --> rolePacXyz00Tenant -rolePacXyz00Admin --> permPacXyz00AddUser -rolePacXyz00Admin --> permPacXyz00Edit +rolePacXyz00Admin --> permPacXyz00INSERT:user +rolePacXyz00Admin --> permPacXyz00UPDATE actor "Package XYZ00 Admin" as actorPacXyzAdmin actorPacXyzAdmin -l-> rolePacXyz00Admin @@ -624,10 +620,10 @@ Let's have a look at the two view queries: WHERE target.uuid IN ( SELECT uuid FROM queryAccessibleObjectUuidsOfSubjectIds( - 'view', 'customer', currentSubjectsUuids())); + 'SELECT, 'customer', currentSubjectsUuids())); This view should be automatically updatable. -Where, for updates, we actually have to check for 'edit' instead of 'view' operation, which makes it a bit more complicated. +Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECT' operation, which makes it a bit more complicated. With the larger dataset, the test suite initially needed over 7 seconds with this view query. At this point the second variant was tried. @@ -642,7 +638,7 @@ Looks like the query optimizer needed some statistics to find the best path. SELECT DISTINCT target.* FROM customer AS target JOIN queryAccessibleObjectUuidsOfSubjectIds( - 'view', 'customer', currentSubjectsUuids()) AS allowedObjId + 'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId ON target.uuid = allowedObjId; This view cannot is not updatable automatically, @@ -688,7 +684,7 @@ Otherwise, it would not be possible to assign roles to new users. All roles are system-defined and cannot be created or modified by any external API. -Users can view only the roles to which they are assigned. +Users can view only the roles to which are granted to them. ## RbacGrant diff --git a/sql/rbac-tests.sql b/sql/rbac-tests.sql index 4e179dee..e30ac926 100644 --- a/sql/rbac-tests.sql +++ b/sql/rbac-tests.sql @@ -25,7 +25,7 @@ FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('customer', select * FROM queryAllRbacUsersWithPermissionsFor(findEffectivePermissionId('package', (SELECT uuid FROM RbacObject WHERE objectTable = 'package' LIMIT 1), - 'delete')); + 'DELETE')); DO LANGUAGE plpgsql $$ @@ -34,12 +34,12 @@ $$ result bool; BEGIN userId = findRbacUser('superuser-alex@hostsharing.net'); - result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'add-package'), userId)); + result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'add-package'), userId)); IF (result) THEN RAISE EXCEPTION 'expected permission NOT to be granted, but it is'; end if; - result = (SELECT * FROM isPermissionGrantedToSubject(findEffectivePermissionId('package', 94928, 'view'), userId)); + result = (SELECT * FROM isPermissionGrantedToSubject(findPermissionId('package', 94928, 'SELECT'), userId)); IF (NOT result) THEN RAISE EXCEPTION 'expected permission to be granted, but it is NOT'; end if; diff --git a/sql/rbac-view-option-experiments.sql b/sql/rbac-view-option-experiments.sql index d3ef736a..f6e80e10 100644 --- a/sql/rbac-view-option-experiments.sql +++ b/sql/rbac-view-option-experiments.sql @@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer TO restricted USING ( -- id=1000 - isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid()) + isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid()) ); SET SESSION AUTHORIZATION restricted; @@ -35,7 +35,7 @@ SELECT * FROM customer; CREATE OR REPLACE RULE "_RETURN" AS ON SELECT TO cust_view DO INSTEAD - SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'view'), currentUserUuid()); + SELECT * FROM customer WHERE isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid()); SELECT * from cust_view LIMIT 10; select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net')); @@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS DO INSTEAD SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectTable='test_customer' AND p.objectUuid=c.uuid AND p.op in ('*', 'view'); + ON p.objectTable='test_customer' AND p.objectUuid=c.uuid; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS SELECT c.uuid, c.reference, c.prefix FROM customer AS c JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p - ON p.objectUuid=c.uuid AND p.op in ('*', 'view'); + ON p.objectUuid=c.uuid; GRANT ALL PRIVILEGES ON cust_view TO restricted; SET SESSION SESSION AUTHORIZATION restricted; @@ -81,7 +81,7 @@ select rr.uuid, rr.type from RbacGrants g join RbacReference RR on g.ascendantUuid = RR.uuid where g.descendantUuid in ( select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com')) - where objectTable='test_customer' and op in ('*', 'view')); + where objectTable='test_customer'); call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com')); diff --git a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java index e20d1357..deeae9f8 100644 --- a/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java +++ b/src/main/java/net/hostsharing/hsadminng/errors/ReferenceNotFoundException.java @@ -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 ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { + public ReferenceNotFoundException(final Class entityClass, final UUID uuid, final Throwable exc) { super(exc); this.entityClass = entityClass; this.uuid = uuid; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java index 4d067f68..de256ca1 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountEntity.java @@ -4,6 +4,7 @@ import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.persistence.HasUuid; +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 @@ -50,4 +56,25 @@ public class HsOfficeBankAccountEntity implements HasUuid, Stringifyable { public String toShortString() { return holder; } + + public static RbacView rbac() { + return rbacViewFor("bankAccount", HsOfficeBankAccountEntity.class) + .withIdentityView(SQL.projection("iban || ':' || holder")) + .withUpdatableColumns("holder", "iban", "bic") + .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("243-hs-office-bankaccount-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java index 69555dc4..406b232c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactEntity.java @@ -4,13 +4,21 @@ import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.persistence.HasUuid; +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 @@ -28,7 +36,6 @@ public class HsOfficeContactEntity implements Stringifyable, HasUuid { .withProp(Fields.label, HsOfficeContactEntity::getLabel) .withProp(Fields.emailAddresses, HsOfficeContactEntity::getEmailAddresses); - @Id @GeneratedValue(generator = "UUID") @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator") @@ -53,4 +60,25 @@ 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); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("203-hs-office-contact-rbac-generated"); + } } 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 76480ac0..29a9452d 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 @@ -4,16 +4,25 @@ 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.relationship.HsOfficeRelationshipEntity; +import net.hostsharing.hsadminng.persistence.HasUuid; +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.Optional; import java.util.UUID; +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.Role.*; +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 @@ -78,7 +87,7 @@ public class HsOfficeDebitorEntity implements HasUuid, Stringifyable { private String defaultPrefix; private String getDebitorNumberString() { - if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null ) { + if (partner == null || partner.getPartnerNumber() == null || debitorNumberSuffix == null) { return null; } return partner.getPartnerNumber() + String.format("%02d", debitorNumberSuffix); @@ -97,4 +106,71 @@ 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, + 'D-' || (SELECT partner.partnerNumber + FROM hs_office_partner partner + JOIN hs_office_relationship partnerRel + ON partnerRel.uuid = partner.partnerRoleUUid AND partnerRel.relType = 'PARTNER' + JOIN hs_office_relationship debitorRel + ON debitorRel.relAnchorUuid = partnerRel.relHolderUuid AND partnerRel.relType = 'ACCOUNTING' + WHERE debitorRel.uuid = debitor.debitorRelUuid) + || to_char(debitorNumberSuffix, 'fm00') + from hs_office_debitor as debitor + """)) + .withUpdatableColumns( + "debitorRel", + "billable", + "debitorUuid", + "refundBankAccountUuid", + "vatId", + "vatCountryCode", + "vatBusiness", + "vatReverseCharge", + "defaultPrefix" /* TODO: do we want that updatable? */) + .createPermission(custom("new-debitor")).grantedTo("global", ADMIN) + + .importRootEntityAliasProxy("debitorRel", HsOfficeRelationshipEntity.class, + fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid + """), + dependsOnColumn("debitorRelUuid")) + .createPermission(DELETE).grantedTo("debitorRel", OWNER) + .createPermission(UPDATE).grantedTo("debitorRel", ADMIN) + .createPermission(SELECT).grantedTo("debitorRel", TENANT) + + .importEntityAlias("refundBankAccount", HsOfficeBankAccountEntity.class, + dependsOnColumn("refundBankAccountUuid"), fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS r + WHERE r.relType = 'ACCOUNTING' AND r.relHolderUuid = ${REF}.debitorRelUuid + """) + ) + .toRole("refundBankAccount", ADMIN).grantRole("debitorRel", AGENT) + .toRole("debitorRel", AGENT).grantRole("refundBankAccount", REFERRER) + + .importEntityAlias("partnerRel", HsOfficeRelationshipEntity.class, + dependsOnColumn("partnerRelUuid"), fetchedBySql(""" + SELECT * + FROM hs_office_relationship AS partnerRel + WHERE ${debitorRel}.relAnchorUuid = partnerRel.relHolderUuid + """) + ) + .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("273-hs-office-debitor-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java index 04dcbb6a..6fdd0732 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerController.java @@ -8,12 +8,12 @@ import net.hostsharing.hsadminng.hs.office.generated.api.v1.model.HsOfficePartne 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.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.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; @@ -158,7 +158,7 @@ public class HsOfficePartnerController implements HsOfficePartnersApi { return entity; } - private E ref(final Class entityClass, final UUID uuid) { + private E ref(final Class entityClass, final UUID uuid) { try { return em.getReference(entityClass, uuid); } catch (final Throwable exc) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java index 55b30148..e557f9ae 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerDetailsEntity.java @@ -2,14 +2,23 @@ package net.hostsharing.hsadminng.hs.office.partner; import lombok.*; import net.hostsharing.hsadminng.errors.DisplayName; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; +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.Column.dependsOnColumn; +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; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -55,6 +64,45 @@ public class HsOfficePartnerDetailsEntity implements HasUuid, Stringifyable { return registrationNumber != null ? registrationNumber : birthName != null ? birthName : birthday != null ? birthday.toString() - : dateOfDeath != null ? dateOfDeath.toString() : ""; + : dateOfDeath != null ? dateOfDeath.toString() + : ""; + } + + + public static RbacView rbac() { + return rbacViewFor("partnerDetails", HsOfficePartnerDetailsEntity.class) + .withIdentityView(SQL.query(""" + SELECT partner_iv.idName || '-details' + 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 + """)) + .withUpdatableColumns( + "registrationOffice", + "registrationNumber", + "birthPlace", + "birthName", + "birthday", + "dateOfDeath") + .createPermission(custom("new-partner-details")).grantedTo("global", ADMIN) + + .importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class, + fetchedBySql(""" + SELECT partnerRel.* + FROM hs_office_relationship AS partnerRel + JOIN hs_office_partner AS partner + ON partner.detailsUuid = ${ref}.uuid + WHERE partnerRel.uuid = partner.partnerRoleUuid + """), + dependsOnColumn("partnerRoleUuid")) + + // 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("234-hs-office-partner-details-rbac-generated"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java index 342b601c..aa000f67 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerEntity.java @@ -6,15 +6,24 @@ 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.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.io.IOException; 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.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.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -68,4 +77,37 @@ public class HsOfficePartnerEntity implements Stringifyable, HasUuid { public String toShortString() { return Optional.ofNullable(person).map(HsOfficePersonEntity::toShortString).orElse(""); } + + public static RbacView rbac() { + return rbacViewFor("partner", HsOfficePartnerEntity.class) + .withIdentityView(SQL.query(""" + SELECT partner.partnerNumber + || ':' || (SELECT idName FROM hs_office_person_iv p WHERE p.uuid = partner.personUuid) + || '-' || (SELECT idName FROM hs_office_contact_iv c WHERE c.uuid = partner.contactUuid) + FROM hs_office_partner AS partner + """)) + .withUpdatableColumns( + "partnerRoleUuid", + "personUuid", + "contactUuid") + .createPermission(custom("new-partner")).grantedTo("global", ADMIN) + + .importRootEntityAliasProxy("partnerRel", HsOfficeRelationshipEntity.class, + fetchedBySql("SELECT * FROM hs_office_relationship AS r WHERE r.uuid = ${ref}.partnerRoleUuid"), + dependsOnColumn("partnerRelUuid")) + .createPermission(DELETE).grantedTo("partnerRel", ADMIN) + .createPermission(UPDATE).grantedTo("partnerRel", AGENT) + .createPermission(SELECT).grantedTo("partnerRel", TENANT) + + .importSubEntityAlias("partnerDetails", HsOfficePartnerDetailsEntity.class, + fetchedBySql("SELECT * FROM hs_office_partner_details AS d WHERE d.uuid = ${ref}.detailsUuid"), + 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("233-hs-office-partner-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java index fde3972b..fcc89dde 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonEntity.java @@ -4,13 +4,21 @@ import lombok.*; import lombok.experimental.FieldNameConstants; import net.hostsharing.hsadminng.errors.DisplayName; import net.hostsharing.hsadminng.persistence.HasUuid; +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 @@ -56,4 +64,26 @@ public class HsOfficePersonEntity implements HasUuid, Stringifyable { return personType + " " + (!StringUtils.isEmpty(tradeName) ? tradeName : (familyName + ", " + givenName)); } + + public static RbacView rbac() { + return rbacViewFor("person", HsOfficePersonEntity.class) + .withIdentityView(SQL.projection("concat(tradeName, familyName, givenName)")) + .withUpdatableColumns("personType", "tradeName", "givenName", "familyName") + .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("213-hs-office-person-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java index 704f2760..1ec9fd74 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipEntity.java @@ -3,14 +3,24 @@ 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.persistence.HasUuid; +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.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.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.stringify.Stringify.stringify; @Entity @@ -67,4 +77,52 @@ public class HsOfficeRelationshipEntity implements HasUuid, Stringifyable { public String toShortString() { return toShortString.apply(this); } + + public static RbacView rbac() { + return rbacViewFor("relationship", HsOfficeRelationshipEntity.class) + .withIdentityView(SQL.projection(""" + (select idName from hs_office_person_iv p where p.uuid = relAnchorUuid) + || '-with-' || target.relType || '-' + || (select idName from hs_office_person_iv p where p.uuid = relHolderUuid) + """)) + .withRestrictedViewOrderBy(SQL.expression( + "(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)")) + .withUpdatableColumns("contactUuid") + .importEntityAlias("anchorPerson", HsOfficePersonEntity.class, + dependsOnColumn("relAnchorUuid"), + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relAnchorUuid") + ) + .importEntityAlias("holderPerson", HsOfficePersonEntity.class, + dependsOnColumn("relHolderUuid"), + fetchedBySql("select * from hs_office_person as p where p.uuid = ${REF}.relHolderUuid") + ) + .importEntityAlias("contact", HsOfficeContactEntity.class, + dependsOnColumn("contactUuid"), + fetchedBySql("select * from hs_office_contact as c where c.uuid = ${REF}.contactUuid") + ) + .createRole(OWNER, (with) -> { + with.owningUser(CREATOR); + with.incomingSuperRole(GLOBAL, ADMIN); + with.permission(DELETE); + }) + .createSubRole(ADMIN, (with) -> { + with.incomingSuperRole("anchorPerson", ADMIN); + 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); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("223-hs-office-relationship-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java index baed26aa..7fcef622 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateEntity.java @@ -6,16 +6,26 @@ 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.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.persistence.HasUuid; +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.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 @@ -84,4 +94,35 @@ public class HsOfficeSepaMandateEntity implements Stringifyable, HasUuid { return reference; } + public static RbacView rbac() { + return rbacViewFor("sepaMandate", HsOfficeSepaMandateEntity.class) + .withIdentityView(projection("concat(tradeName, familyName, givenName)")) + .withUpdatableColumns("reference", "agreement", "validity") + + .importEntityAlias("debitorRel", HsOfficeRelationshipEntity.class, dependsOnColumn("debitorRelUuid")) + .importEntityAlias("bankAccount", HsOfficeBankAccountEntity.class, dependsOnColumn("bankAccountUuid")) + + .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); + }); + } + + public static void main(String[] args) throws IOException { + rbac().generateWithBaseFileName("253-hs-office-sepamandate-rbac-generated"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java index 1f3ead14..03e6abf3 100644 --- a/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java +++ b/src/main/java/net/hostsharing/hsadminng/persistence/HasUuid.java @@ -1,7 +1,7 @@ package net.hostsharing.hsadminng.persistence; -import java.util.UUID; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; -public interface HasUuid { - UUID getUuid(); +// TODO: remove this interface, I just wanted to avoid to many changes in that PR +public interface HasUuid extends RbacObject { } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java new file mode 100644 index 00000000..5303c27e --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/InsertTriggerGenerator.java @@ -0,0 +1,165 @@ +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.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); + generateGrantInsertRoleToExistingCustomers(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 generateGrantInsertRoleToExistingCustomers(final StringWriter plPgSql) { + getOptionalInsertSuperRole().ifPresent( superRoleDef -> { + plPgSql.writeLn(""" + /* + Creates INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows. + */ + do language plpgsql $$ + declare + row ${rawSuperTableName}; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO ${rawSubTableName} permissions for the related ${rawSuperTableName} rows'); + + FOR row IN SELECT * FROM ${rawSuperTableName} + LOOP + roleUuid := findRoleId(${rawSuperRoleDescriptor}(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', '${rawSubTableName}'); + call grantPermissionToRole(roleUuid, permissionUuid); + END LOOP; + END; + $$; + """, + with("rawSubTableName", rbacDef.getRootEntityAlias().getRawTableName()), + with("rawSuperTableName", superRoleDef.getEntityAlias().getRawTableName()), + with("rawSuperRoleDescriptor", toVar(superRoleDef)) + ); + }); + } + + 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( + ${rawSuperRoleDescriptor}(NEW), + createPermission(NEW.uuid, 'INSERT', '${rawSubTableName}')); + return NEW; + end; $$; + + create trigger ${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", toVar(superRoleDef)) + ); + }); + } + + private void generateInsertCheckTrigger(final StringWriter plPgSql) { + plPgSql.writeLn(""" + /** + Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. + */ + 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; $$; + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + getOptionalInsertGrant().ifPresentOrElse(g -> { + plPgSql.writeLn(""" + 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() )); + }, + () -> { + plPgSql.writeLn(""" + create trigger ${rawSubTable}_insert_permission_check_tg + before insert on ${rawSubTable} + for each row + -- As there is no explicit INSERT grant specified for this table, + -- only global admins are allowed to insert any rows. + when ( not isGlobalAdmin() ) + execute procedure ${rawSubTable}_insert_permission_missing_tf(); + """, + with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName())); + }); + } + + private Stream getInsertGrants() { + return rbacDef.getGrantDefs().stream() + .filter(g -> g.grantType() == PERM_TO_ROLE) + .filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT); + } + + private Optional getOptionalInsertGrant() { + return getInsertGrants() + .reduce(singleton()); + } + + private Optional getOptionalInsertSuperRole() { + return getInsertGrants() + .map(RbacView.RbacGrantDefinition::getSuperRoleDef) + .reduce(singleton()); + } + + private static BinaryOperator 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().roleName()); + } + +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java new file mode 100644 index 00000000..4fb5cb61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/PostgresTriggerReference.java @@ -0,0 +1,5 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +public enum PostgresTriggerReference { + NEW, OLD +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java new file mode 100644 index 00000000..d664a83b --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacIdentityViewGenerator.java @@ -0,0 +1,45 @@ +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 generateRbacIdentityViewFromProjection('${rawTableName}', $idName$ + ${identityViewSqlPart} + $idName$); + """; + default -> throw new IllegalStateException("illegal SQL part given"); + }, + with("identityViewSqlPart", rbacDef.getIdentityViewSqlQuery().sql), + with("rawTableName", rawTableName)); + + plPgSql.writeLn("--//"); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java new file mode 100644 index 00000000..a7377301 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacObjectGenerator.java @@ -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)); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java new file mode 100644 index 00000000..f8f6e890 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRestrictedViewGenerator.java @@ -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 simpleEntityVarName; + private final String rawTableName; + + public RbacRestrictedViewGenerator(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-RESTRICTED-VIEW:1 endDelimiter:--// + -- ---------------------------------------------------------------------------- + call generateRbacRestrictedView('${rawTableName}', + '${orderBy}', + $updates$ + ${updates} + $updates$); + --// + + """, + with("liquibaseTagPrefix", liquibaseTagPrefix), + with("orderBy", rbacDef.getOrderBySqlExpression().sql), + with("updates", indented(rbacDef.getUpdatableColumns().stream() + .map(c -> c + " = new." + c) + .collect(joining(",\n")), 2)), + with("rawTableName", rawTableName)); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java new file mode 100644 index 00000000..dab3ab01 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacRoleDescriptorsGenerator.java @@ -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)); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java new file mode 100644 index 00000000..28d29365 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacView.java @@ -0,0 +1,830 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import net.hostsharing.hsadminng.hs.office.bankaccount.HsOfficeBankAccountEntity; +import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactEntity; +import net.hostsharing.hsadminng.hs.office.coopassets.HsOfficeCoopAssetsTransactionEntity; +import net.hostsharing.hsadminng.hs.office.coopshares.HsOfficeCoopSharesTransactionEntity; +import net.hostsharing.hsadminng.hs.office.debitor.HsOfficeDebitorEntity; +import net.hostsharing.hsadminng.hs.office.membership.HsOfficeMembershipEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerDetailsEntity; +import net.hostsharing.hsadminng.hs.office.partner.HsOfficePartnerEntity; +import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonEntity; +import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; +import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; +import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; +import net.hostsharing.hsadminng.test.cust.TestCustomerEntity; +import net.hostsharing.hsadminng.test.dom.TestDomainEntity; +import net.hostsharing.hsadminng.test.pac.TestPackageEntity; + +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import jakarta.validation.constraints.NotNull; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import static java.lang.reflect.Modifier.isStatic; +import static java.util.Arrays.stream; +import static java.util.Optional.ofNullable; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.RbacUserReference.UserRole.CREATOR; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.autoFetched; +import static org.apache.commons.lang3.StringUtils.uncapitalize; + +@Getter +public class RbacView { + + public static final String GLOBAL = "global"; + public static final String OUTPUT_BASEDIR = "src/main/resources/db/changelog"; + + private final EntityAlias rootEntityAlias; + + private final Set userDefs = new LinkedHashSet<>(); + private final Set roleDefs = new LinkedHashSet<>(); + private final Set permDefs = new LinkedHashSet<>(); + private final Map entityAliases = new HashMap<>() { + + @Override + public EntityAlias put(final String key, final EntityAlias value) { + if (containsKey(key)) { + throw new IllegalArgumentException("duplicate entityAlias: " + key); + } + return super.put(key, value); + } + }; + private final Set updatableColumns = new LinkedHashSet<>(); + private final Set grantDefs = new LinkedHashSet<>(); + + private SQL identityViewSqlQuery; + private SQL orderBySqlExpression; + private EntityAlias rootEntityAliasProxy; + private RbacRoleDefinition previousRoleDef; + + public static RbacView rbacViewFor(final String alias, final Class entityClass) { + return new RbacView(alias, entityClass); + } + + RbacView(final String alias, final Class entityClass) { + rootEntityAlias = new EntityAlias(alias, entityClass); + entityAliases.put(alias, rootEntityAlias); + new RbacUserReference(CREATOR); + entityAliases.put("global", new EntityAlias("global")); + } + + public RbacView withUpdatableColumns(final String... columnNames) { + Collections.addAll(updatableColumns, columnNames); + verifyVersionColumnExists(); + return this; + } + + public RbacView withIdentityView(final SQL sqlExpression) { + this.identityViewSqlQuery = sqlExpression; + return this; + } + + public RbacView withRestrictedViewOrderBy(final SQL orderBySqlExpression) { + this.orderBySqlExpression = orderBySqlExpression; + return this; + } + + public RbacView createRole(final Role role, final Consumer with) { + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); + with.accept(newRoleDef); + previousRoleDef = newRoleDef; + return this; + } + + public RbacView createSubRole(final Role role) { + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); + findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); + previousRoleDef = newRoleDef; + return this; + } + + public RbacView createSubRole(final Role role, final Consumer with) { + final RbacRoleDefinition newRoleDef = findRbacRole(rootEntityAlias, role).toCreate(); + findOrCreateGrantDef(newRoleDef, previousRoleDef).toCreate(); + with.accept(newRoleDef); + previousRoleDef = newRoleDef; + return this; + } + + public RbacPermissionDefinition createPermission(final Permission permission) { + return createPermission(rootEntityAlias, permission); + } + + public RbacPermissionDefinition createPermission(final String entityAliasName, final Permission permission) { + return createPermission(findEntityAlias(entityAliasName), permission); + } + + private RbacPermissionDefinition createPermission(final EntityAlias entityAlias, final Permission permission) { + return new RbacPermissionDefinition(entityAlias, permission, null, true); + } + + public RbacView declarePlaceholderEntityAliases(final String... aliasNames) { + for (String alias : aliasNames) { + entityAliases.put(alias, new EntityAlias(alias)); + } + return this; + } + + public RbacView importRootEntityAliasProxy( + final String aliasName, + final Class entityClass, + final SQL fetchSql, + final Column dependsOnColum) { + if (rootEntityAliasProxy != null) { + throw new IllegalStateException("there is already an entityAliasProxy: " + rootEntityAliasProxy); + } + rootEntityAliasProxy = importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); + return this; + } + + public RbacView importSubEntityAlias( + final String aliasName, final Class entityClass, + final SQL fetchSql, final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, true); + return this; + } + + public RbacView importEntityAlias( + final String aliasName, final Class entityClass, + final Column dependsOnColum, final SQL fetchSql) { + importEntityAliasImpl(aliasName, entityClass, fetchSql, dependsOnColum, false); + return this; + } + + public RbacView importEntityAlias( + final String aliasName, final Class entityClass, + final Column dependsOnColum) { + importEntityAliasImpl(aliasName, entityClass, autoFetched(), dependsOnColum, false); + return this; + } + + private EntityAlias importEntityAliasImpl( + final String aliasName, final Class entityClass, + final SQL fetchSql, final Column dependsOnColum, boolean asSubEntity) { + final var entityAlias = new EntityAlias(aliasName, entityClass, fetchSql, dependsOnColum, asSubEntity); + entityAliases.put(aliasName, entityAlias); + try { + importAsAlias(aliasName, rbacDefinition(entityClass), asSubEntity); + } catch (final ReflectiveOperationException exc) { + throw new RuntimeException("cannot import entity: " + entityClass, exc); + } + return entityAlias; + } + + private static RbacView rbacDefinition(final Class entityClass) + throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { + return (RbacView) entityClass.getMethod("rbac").invoke(null); + } + + private RbacView importAsAlias(final String aliasName, final RbacView importedRbacView, final boolean asSubEntity) { + final var mapper = new AliasNameMapper(importedRbacView, aliasName, + asSubEntity ? entityAliases.keySet() : null); + importedRbacView.getEntityAliases().values().stream() + .filter(entityAlias -> !importedRbacView.isRootEntityAlias(entityAlias)) + .filter(entityAlias -> !entityAlias.isGlobal()) + .filter(entityAlias -> !asSubEntity || !entityAliases.containsKey(entityAlias.aliasName)) + .forEach(entityAlias -> { + final String mappedAliasName = mapper.map(entityAlias.aliasName); + entityAliases.put(mappedAliasName, new EntityAlias(mappedAliasName, entityAlias.entityClass)); + }); + importedRbacView.getRoleDefs().forEach(roleDef -> { + new RbacRoleDefinition(findEntityAlias(mapper.map(roleDef.entityAlias.aliasName)), roleDef.role); + }); + importedRbacView.getGrantDefs().forEach(grantDef -> { + if (grantDef.grantType() == RbacGrantDefinition.GrantType.ROLE_TO_ROLE) { + final var importedGrantDef = findOrCreateGrantDef( + findRbacRole( + mapper.map(grantDef.getSubRoleDef().entityAlias.aliasName), + grantDef.getSubRoleDef().getRole()), + findRbacRole( + mapper.map(grantDef.getSuperRoleDef().entityAlias.aliasName), + grantDef.getSuperRoleDef().getRole()) + ); + if (!grantDef.isAssumed()) { + importedGrantDef.unassumed(); + } + } + }); + return this; + } + + private void verifyVersionColumnExists() { + if (stream(rootEntityAlias.entityClass.getDeclaredFields()) + .noneMatch(f -> f.getAnnotation(Version.class) != null)) { + // TODO: convert this into throw Exception once RbacEntity is a base class with @Version field + System.err.println("@Version field required in updatable entity " + rootEntityAlias.entityClass); + } + } + + public RbacGrantBuilder toRole(final String entityAlias, final Role role) { + return new RbacGrantBuilder(entityAlias, role); + } + + public RbacExampleRole forExampleRole(final String entityAlias, final Role role) { + return new RbacExampleRole(entityAlias, role); + } + + private RbacGrantDefinition grantRoleToUser(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { + return findOrCreateGrantDef(roleDefinition, user).toCreate(); + } + + private RbacGrantDefinition grantPermissionToRole( + final RbacPermissionDefinition permDef, + final RbacRoleDefinition roleDef) { + return findOrCreateGrantDef(permDef, roleDef).toCreate(); + } + + private RbacGrantDefinition grantSubRoleToSuperRole( + final RbacRoleDefinition subRoleDefinition, + final RbacRoleDefinition superRoleDefinition) { + return findOrCreateGrantDef(subRoleDefinition, superRoleDefinition).toCreate(); + } + + boolean isRootEntityAlias(final EntityAlias entityAlias) { + return entityAlias == this.rootEntityAlias; + } + + public boolean isEntityAliasProxy(final EntityAlias entityAlias) { + return entityAlias == rootEntityAliasProxy; + } + + public SQL getOrderBySqlExpression() { + if (orderBySqlExpression == null) { + return identityViewSqlQuery; + } + return orderBySqlExpression; + } + + public void generateWithBaseFileName(final String baseFileName) { + new RbacViewMermaidFlowchartGenerator(this).generateToMarkdownFile(Path.of(OUTPUT_BASEDIR, baseFileName + ".md")); + new RbacViewPostgresGenerator(this).generateToChangeLog(Path.of(OUTPUT_BASEDIR, baseFileName + ".sql")); + } + + public class RbacGrantBuilder { + + private final RbacRoleDefinition superRoleDef; + + private RbacGrantBuilder(final String entityAlias, final Role role) { + this.superRoleDef = findRbacRole(entityAlias, role); + } + + public RbacView grantRole(final String entityAlias, final Role role) { + findOrCreateGrantDef(findRbacRole(entityAlias, role), superRoleDef).toCreate(); + return RbacView.this; + } + + public RbacView grantPermission(final String entityAliasName, final Permission perm) { + final var entityAlias = findEntityAlias(entityAliasName); + final var forTable = entityAlias.getRawTableName(); + findOrCreateGrantDef(findRbacPerm(entityAlias, perm, forTable), superRoleDef).toCreate(); + return RbacView.this; + } + + } + + @Getter + @EqualsAndHashCode + public class RbacGrantDefinition { + + private final RbacUserReference userDef; + private final RbacRoleDefinition superRoleDef; + private final RbacRoleDefinition subRoleDef; + private final RbacPermissionDefinition permDef; + private boolean assumed = true; + private boolean toCreate = false; + + @Override + public String toString() { + final var arrow = isAssumed() ? " --> " : " -- // --> "; + return switch (grantType()) { + case ROLE_TO_USER -> userDef.toString() + arrow + subRoleDef.toString(); + case ROLE_TO_ROLE -> superRoleDef + arrow + subRoleDef; + case PERM_TO_ROLE -> superRoleDef + arrow + permDef; + }; + } + + RbacGrantDefinition(final RbacRoleDefinition subRoleDef, final RbacRoleDefinition superRoleDef) { + this.userDef = null; + this.subRoleDef = subRoleDef; + this.superRoleDef = superRoleDef; + this.permDef = null; + register(this); + } + + public RbacGrantDefinition(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { + this.userDef = null; + this.subRoleDef = null; + this.superRoleDef = roleDef; + this.permDef = permDef; + register(this); + } + + public RbacGrantDefinition(final RbacRoleDefinition roleDef, final RbacUserReference userDef) { + this.userDef = userDef; + this.subRoleDef = roleDef; + this.superRoleDef = null; + this.permDef = null; + register(this); + } + + private void register(final RbacGrantDefinition rbacGrantDefinition) { + grantDefs.add(rbacGrantDefinition); + } + + @NotNull + GrantType grantType() { + return permDef != null ? GrantType.PERM_TO_ROLE + : userDef != null ? GrantType.ROLE_TO_USER + : GrantType.ROLE_TO_ROLE; + } + + boolean isAssumed() { + return assumed; + } + + boolean isToCreate() { + return toCreate; + } + + RbacGrantDefinition toCreate() { + toCreate = true; + return this; + } + + boolean dependsOnColumn(final String columnName) { + return dependsRoleDefOnColumnName(this.superRoleDef, columnName) + || dependsRoleDefOnColumnName(this.subRoleDef, columnName); + } + + private Boolean dependsRoleDefOnColumnName(final RbacRoleDefinition superRoleDef, final String columnName) { + return ofNullable(superRoleDef) + .map(r -> r.getEntityAlias().dependsOnColum()) + .map(d -> columnName.equals(d.column)) + .orElse(false); + } + + public void unassumed() { + this.assumed = false; + } + + public enum GrantType { + ROLE_TO_USER, + ROLE_TO_ROLE, + PERM_TO_ROLE + } + } + + public class RbacExampleRole { + + final EntityAlias subRoleEntity; + final Role subRole; + private EntityAlias superRoleEntity; + Role superRole; + + public RbacExampleRole(final String entityAlias, final Role role) { + this.subRoleEntity = findEntityAlias(entityAlias); + this.subRole = role; + } + + public RbacView wouldBeGrantedTo(final String entityAlias, final Role role) { + this.superRoleEntity = findEntityAlias(entityAlias); + this.superRole = role; + return RbacView.this; + } + } + + @Getter + @EqualsAndHashCode + public class RbacPermissionDefinition { + + final EntityAlias entityAlias; + final Permission permission; + final String tableName; + final boolean toCreate; + + private RbacPermissionDefinition(final EntityAlias entityAlias, final Permission permission, final String tableName, final boolean toCreate) { + this.entityAlias = entityAlias; + this.permission = permission; + this.tableName = tableName; + this.toCreate = toCreate; + permDefs.add(this); + } + + public RbacView grantedTo(final String entityAlias, final Role role) { + findOrCreateGrantDef(this, findRbacRole(entityAlias, role)).toCreate(); + return RbacView.this; + } + + @Override + public String toString() { + return "perm:" + entityAlias.aliasName + permission + ofNullable(tableName).map(tn -> ":" + tn).orElse(""); + } + } + + @Getter + @EqualsAndHashCode + public class RbacRoleDefinition { + + private final EntityAlias entityAlias; + private final Role role; + private boolean toCreate; + + public RbacRoleDefinition(final EntityAlias entityAlias, final Role role) { + this.entityAlias = entityAlias; + this.role = role; + roleDefs.add(this); + } + + public RbacRoleDefinition toCreate() { + this.toCreate = true; + return this; + } + + public RbacGrantDefinition owningUser(final RbacUserReference.UserRole userRole) { + return grantRoleToUser(this, findUserRef(userRole)); + } + + public RbacGrantDefinition permission(final Permission permission) { + return grantPermissionToRole(createPermission(entityAlias, permission), this); + } + + public RbacGrantDefinition incomingSuperRole(final String entityAlias, final Role role) { + final var incomingSuperRole = findRbacRole(entityAlias, role); + return grantSubRoleToSuperRole(this, incomingSuperRole); + } + + public RbacGrantDefinition outgoingSubRole(final String entityAlias, final Role role) { + final var outgoingSubRole = findRbacRole(entityAlias, role); + return grantSubRoleToSuperRole(outgoingSubRole, this); + } + + @Override + public String toString() { + return "role:" + entityAlias.aliasName + role; + } + } + + public RbacUserReference findUserRef(final RbacUserReference.UserRole userRole) { + return userDefs.stream().filter(u -> u.role == userRole).findFirst().orElseThrow(); + } + + @EqualsAndHashCode + public class RbacUserReference { + + public enum UserRole { + GLOBAL_ADMIN, + CREATOR + } + + final UserRole role; + + public RbacUserReference(final UserRole creator) { + this.role = creator; + userDefs.add(this); + } + + @Override + public String toString() { + return "user:" + role; + } + } + + EntityAlias findEntityAlias(final String aliasName) { + final var found = entityAliases.get(aliasName); + if (found == null) { + throw new IllegalArgumentException("entityAlias not found: " + aliasName); + } + return found; + } + + RbacRoleDefinition findRbacRole(final EntityAlias entityAlias, final Role role) { + return roleDefs.stream() + .filter(r -> r.getEntityAlias() == entityAlias && r.getRole().equals(role)) + .findFirst() + .orElseGet(() -> new RbacRoleDefinition(entityAlias, role)); + } + + public RbacRoleDefinition findRbacRole(final String entityAliasName, final Role role) { + return findRbacRole(findEntityAlias(entityAliasName), role); + + } + + RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm, String tableName) { + return permDefs.stream() + .filter(p -> p.getEntityAlias() == entityAlias && p.getPermission() == perm) + .findFirst() + .orElseGet(() -> new RbacPermissionDefinition(entityAlias, perm, tableName, true)); // TODO: true => toCreate + } + + + RbacPermissionDefinition findRbacPerm(final EntityAlias entityAlias, final Permission perm) { + return findRbacPerm(entityAlias, perm, null); + } + + public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm, String tableName) { + return findRbacPerm(findEntityAlias(entityAliasName), perm, tableName); + } + + public RbacPermissionDefinition findRbacPerm(final String entityAliasName, final Permission perm) { + return findRbacPerm(findEntityAlias(entityAliasName), perm); + } + + private RbacGrantDefinition findOrCreateGrantDef(final RbacRoleDefinition roleDefinition, final RbacUserReference user) { + return grantDefs.stream() + .filter(g -> g.subRoleDef == roleDefinition && g.userDef == user) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(roleDefinition, user)); + } + + private RbacGrantDefinition findOrCreateGrantDef(final RbacPermissionDefinition permDef, final RbacRoleDefinition roleDef) { + return grantDefs.stream() + .filter(g -> g.permDef == permDef && g.subRoleDef == roleDef) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(permDef, roleDef)); + } + + private RbacGrantDefinition findOrCreateGrantDef( + final RbacRoleDefinition subRoleDefinition, + final RbacRoleDefinition superRoleDefinition) { + return grantDefs.stream() + .filter(g -> g.subRoleDef == subRoleDefinition && g.superRoleDef == superRoleDefinition) + .findFirst() + .orElseGet(() -> new RbacGrantDefinition(subRoleDefinition, superRoleDefinition)); + } + + record EntityAlias(String aliasName, Class entityClass, SQL fetchSql, Column dependsOnColum, boolean isSubEntity) { + + public EntityAlias(final String aliasName) { + this(aliasName, null, null, null, false); + } + + public EntityAlias(final String aliasName, final Class entityClass) { + this(aliasName, entityClass, null, null, false); + } + + boolean isGlobal() { + return aliasName().equals("global"); + } + + boolean isPlaceholder() { + return entityClass == null; + } + + @NotNull + @Override + public SQL fetchSql() { + if (fetchSql == null) { + return SQL.noop(); + } + return switch (fetchSql.part) { + case SQL_QUERY -> fetchSql; + case AUTO_FETCH -> + SQL.query("SELECT * FROM " + getRawTableName() + " WHERE uuid = ${ref}." + dependsOnColum.column); + default -> throw new IllegalStateException("unexpected SQL definition: " + fetchSql); + }; + } + + public boolean hasFetchSql() { + return fetchSql != null; + } + + private String withoutEntitySuffix(final String simpleEntityName) { + return simpleEntityName.substring(0, simpleEntityName.length() - "Entity".length()); + } + + String simpleName() { + return isGlobal() + ? aliasName + : uncapitalize(withoutEntitySuffix(entityClass.getSimpleName())); + } + + String getRawTableName() { + if ( aliasName.equals("global")) { + return "global"; // TODO: maybe we should introduce a GlobalEntity class? + } + return withoutRvSuffix(entityClass.getAnnotation(Table.class).name()); + } + + String dependsOnColumName() { + if (dependsOnColum == null) { + throw new IllegalStateException( + "Entity " + aliasName + "(" + entityClass.getSimpleName() + ")" + ": please add dependsOnColum"); + } + return dependsOnColum.column; + } + } + + public static String withoutRvSuffix(final String tableName) { + return tableName.substring(0, tableName.length() - "_rv".length()); + } + + public record Role(String roleName) { + + public static final Role OWNER = new Role("owner"); + public static final Role ADMIN = new Role("admin"); + public static final Role AGENT = new Role("agent"); + public static final Role TENANT = new Role("tenant"); + public static final Role REFERRER = new Role("referrer"); + + @Override + public String toString() { + return ":" + roleName; + } + + @Override + public boolean equals(final Object obj) { + return ((obj instanceof Role) && ((Role) obj).roleName.equals(this.roleName)); + } + } + + public record Permission(String permission) { + + public static final Permission INSERT = new Permission("INSERT"); + public static final Permission DELETE = new Permission("DELETE"); + public static final Permission UPDATE = new Permission("UPDATE"); + public static final Permission SELECT = new Permission("SELECT"); + + public static Permission custom(final String permission) { + return new Permission(permission); + } + + @Override + public String toString() { + return ":" + permission; + } + } + + public static class SQL { + + /** + * DSL method to specify an SQL SELECT expression which fetches the related entity, + * using the reference `${ref}` of the root entity. + * `${ref}` is going to be replaced by either `NEW` or `OLD` of the trigger function. + * `into ...` will be added with a variable name prefixed with either `new` or `old`. + * + * @param sql an SQL SELECT expression (not ending with ';) + * @return the wrapped SQL expression + */ + public static SQL fetchedBySql(final String sql) { + validateExpression(sql); + return new SQL(sql, Part.SQL_QUERY); + } + + /** + * DSL method to specify that a related entity is to be fetched by a simple SELECT statement + * using the raw table from the @Table statement of the entity to fetch + * and the dependent column of the root entity. + * + * @return the wrapped SQL definition object + */ + public static SQL autoFetched() { + return new SQL(null, Part.AUTO_FETCH); + } + + /** + * DSL method to explicitly specify that there is no SQL query. + * + * @return a wrapped SQL definition object representing a noop query + */ + public static SQL noop() { + return new SQL(null, Part.NOOP); + } + + /** + * Generic DSL method to specify an SQL SELECT expression. + * + * @param sql an SQL SELECT expression (not ending with ';) + * @return the wrapped SQL expression + */ + public static SQL query(final String sql) { + validateExpression(sql); + return new SQL(sql, Part.SQL_QUERY); + } + + /** + * Generic DSL method to specify an SQL SELECT expression by just the projection part. + * + * @param projection an SQL SELECT expression, the list of columns after 'SELECT' + * @return the wrapped SQL projection + */ + public static SQL projection(final String projection) { + validateProjection(projection); + return new SQL(projection, Part.SQL_PROJECTION); + } + + public static SQL expression(final String sqlExpression) { + // TODO: validate + return new SQL(sqlExpression, Part.SQL_EXPRESSION); + } + + enum Part { + NOOP, + SQL_QUERY, + AUTO_FETCH, + SQL_PROJECTION, + SQL_EXPRESSION + } + + final String sql; + final Part part; + + private SQL(final String sql, final Part part) { + this.sql = sql; + this.part = part; + } + + private static void validateProjection(final String projection) { + if (projection.toUpperCase().matches("[ \t]*$SELECT[ \t]")) { + throw new IllegalArgumentException("SQL projection must not start with 'SELECT': " + projection); + } + if (projection.matches(";[ \t]*$")) { + throw new IllegalArgumentException("SQL projection must not end with ';': " + projection); + } + } + + private static void validateExpression(final String sql) { + if (sql.matches(";[ \t]*$")) { + throw new IllegalArgumentException("SQL expression must not end with ';': " + sql); + } + } + } + + public static class Column { + + public static Column dependsOnColumn(final String column) { + return new Column(column); + } + + public final String column; + + private Column(final String column) { + this.column = column; + } + } + + private static class AliasNameMapper { + + private final RbacView importedRbacView; + private final String outerAliasName; + + private final Set outerAliasNames; + + AliasNameMapper(final RbacView importedRbacView, final String outerAliasName, final Set outerAliasNames) { + this.importedRbacView = importedRbacView; + this.outerAliasName = outerAliasName; + this.outerAliasNames = (outerAliasNames == null) ? Collections.emptySet() : outerAliasNames; + } + + String map(final String originalAliasName) { + if (outerAliasNames.contains(originalAliasName) || originalAliasName.equals("global")) { + return originalAliasName; + } + if (originalAliasName.equals(importedRbacView.rootEntityAlias.aliasName)) { + return outerAliasName; + } + return outerAliasName + "." + originalAliasName; + } + } + + public static void main(String[] args) { + Stream.of( + TestCustomerEntity.class, + TestPackageEntity.class, + TestDomainEntity.class, + HsOfficePersonEntity.class, + HsOfficePartnerEntity.class, + HsOfficePartnerDetailsEntity.class, + HsOfficeBankAccountEntity.class, + HsOfficeDebitorEntity.class, + HsOfficeRelationshipEntity.class, + HsOfficeCoopAssetsTransactionEntity.class, + HsOfficeContactEntity.class, + HsOfficeSepaMandateEntity.class, + HsOfficeCoopSharesTransactionEntity.class, + HsOfficeMembershipEntity.class + ).forEach(c -> { + final Method mainMethod = stream(c.getMethods()).filter( + m -> isStatic(m.getModifiers()) && m.getName().equals("main") + ) + .findFirst() + .orElse(null); + if (mainMethod != null) { + try { + mainMethod.invoke(null, new Object[] { null }); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + System.err.println("no main method in: " + c.getName()); + } + }); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java new file mode 100644 index 00000000..ccef566d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewMermaidFlowchartGenerator.java @@ -0,0 +1,164 @@ +package net.hostsharing.hsadminng.rbac.rbacdef; + +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.*; + +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 at %{timestamp}. + + ```mermaid + %{flowchart} + ``` + """ + .replace("%{entityAlias}", rbacDef.getRootEntityAlias().aliasName()) + .replace("%{timestamp}", LocalDateTime.now().toString()) + .replace("%{flowchart}", flowchart.toString()), + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println("Markdown-File: " + path.toAbsolutePath()); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java new file mode 100644 index 00000000..eb8f3534 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RbacViewPostgresGenerator.java @@ -0,0 +1,52 @@ +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 java.time.LocalDateTime; + +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} at ${timestamp}. + """, + with("generator", getClass().getSimpleName()), + with("timestamp", LocalDateTime.now().toString()), + 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(); +} + + @SneakyThrows + public void generateToChangeLog(final Path outputPath) { + Files.writeString( + outputPath, + toString(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + System.out.println(outputPath.toAbsolutePath()); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java new file mode 100644 index 00000000..edb1f609 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/RolesGrantsAndPermissionsGenerator.java @@ -0,0 +1,507 @@ +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.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 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);"); + generateCreateRolesAndGrantsAfterInsert(plPgSql); + plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);"); + }); + plPgSql.writeLn("end; $$;"); + plPgSql.writeLn(); + } + + 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(() -> { + updatableEntityAliases() + .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);"); + generateUpdateRolesAndGrantsAfterUpdate(plPgSql); + plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn("call leaveTriggerForObjectUuid(NEW.uuid);"); + }); + plPgSql.writeLn("end; $$;"); + plPgSql.writeLn(); + } + + private boolean hasAnyUpdatableEntityAliases() { + return updatableEntityAliases().anyMatch(e -> true); + } + + private void generateCreateRolesAndGrantsAfterInsert(final StringWriter plPgSql) { + referencedEntityAliases() + .forEach((ea) -> plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";", + with("ref", NEW.name()))); + + 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 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 updatableEntityAliases() { + return referencedEntityAliases() + .filter(ea -> rbacDef.getUpdatableColumns().contains(ea.dependsOnColum().column)); + } + + private void generateUpdateRolesAndGrantsAfterUpdate(final StringWriter plPgSql) { + plPgSql.ensureSingleEmptyLine(); + + updatableEntityAliases() + .forEach((ea) -> { + plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(OLD, ea) + ";", + with("ref", OLD.name())); + plPgSql.writeLn( + ea.fetchSql().sql + " into " + entityRefVar(NEW, ea) + ";", + with("ref", NEW.name())); + }); + + 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 boolean isUpdatable(final RbacView.Column c) { + return rbacDef.getUpdatableColumns().contains(c); + } + + private void updateGrantsDependingOn(final StringWriter plPgSql, final String columnName) { + rbacDef.getGrantDefs().stream() + .filter(RbacView.RbacGrantDefinition::isToCreate) + .filter(g -> g.dependsOnColumn(columnName)) + .forEach(g -> { + plPgSql.ensureSingleEmptyLine(); + plPgSql.writeLn(generateRevoke(g)); + plPgSql.writeLn(generateGrant(g)); + plPgSql.writeLn(); + }); + } + + 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}", findPerm(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 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.permission()); + } + + 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().roleName()) + + "(" + 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.roleName()))); + + generatePermissionsForRole(plPgSql, role); + + generateUserGrantsForRole(plPgSql, role); + + generateIncomingSuperRolesForRole(plPgSql, role); + + generateOutgoingSubRolesForRole(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::permission) + .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())) + .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())) + .toList(); + plPgSql.indented(() -> + plPgSql.writeLn("outgoingSubRoles => array[" + joinArrayElements(arrayElements, 1) + "],\n")); + rbacGrants.removeAll(outgoingGrants); + } + } + + private String joinArrayElements(final List arrayElements, final int singleLineLimit) { + return arrayElements.size() <= singleLineLimit + ? String.join(", ", arrayElements) + : arrayElements.stream().collect(joining(",\n\t", "\n\t", "")); + } + + private Set 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 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 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 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"); + 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().roleName()); + } + + private static String toTriggerReference( + final PostgresTriggerReference triggerRef, + final RbacView.EntityAlias entityAlias) { + return triggerRef.name().toLowerCase() + capitalize(entityAlias.aliasName()); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java new file mode 100644 index 00000000..512ec72d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/StringWriter.java @@ -0,0 +1,111 @@ +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 indented(final Runnable indented) { + indent(); + indented.run(); + unindent(); + } + + 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 String text, final int indentLevel) { + 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(text, indentLevel); + } + + 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) { + try { + 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; + } catch (Exception exc) { + throw exc; + } + } + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java new file mode 100644 index 00000000..2a193f2f --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacdef/package-info.java @@ -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. diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java similarity index 89% rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java index 6dc8d1ce..f7b3cdf4 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantEntity.java @@ -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); + } } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java similarity index 67% rename from src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java rename to src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java index c7ac60ab..37828bdf 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RawRbacGrantRepository.java @@ -8,4 +8,8 @@ import java.util.UUID; public interface RawRbacGrantRepository extends Repository { List findAll(); + + List findByAscendingUuid(UUID ascendingUuid); + + List findByDescendantUuid(UUID refUuid); } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java new file mode 100644 index 00000000..0296cd61 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramService.java @@ -0,0 +1,206 @@ +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 { + + 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 + } + + @Autowired + private Context context; + + @Autowired + private RawRbacGrantRepository rawGrantRepo; + + @PersistenceContext + private EntityManager em; + + public String allGrantsToCurrentUser(final EnumSet includes) { + final var graph = new HashSet(); + for ( UUID subjectUuid: context.currentSubjectsUuids() ) { + traverseGrantsTo(graph, subjectUuid, includes); + } + return toMermaidFlowchart(graph, includes); + } + + private void traverseGrantsTo(final Set graph, final UUID refUuid, final EnumSet 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 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 HashSet(); + traverseGrantsFrom(graph, refUuid, includes); + return toMermaidFlowchart(graph, includes); + } + + private void traverseGrantsFrom(final Set graph, final UUID refUuid, final EnumSet 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 graph, final EnumSet 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 " + quoted(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 -> quoted(g.getAscendantIdName()) + + " -->" + (g.isAssumed() ? " " : "|XX| ") + + quoted(g.getDescendantIdName())) + .sorted() + .collect(joining("\n")); + + final var avoidCroppedNodeLabels = "%%{init:{'flowchart':{'htmlLabels':false}}}%%\n\n"; + return (includes.contains(DETAILS) ? avoidCroppedNodeLabels : "") + + "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 "[" + 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 quoted(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 quoted(final String idName) { + return idName.replace(" ", ":").replaceAll("@.*", ""); + } +} + +record Node(String idName, UUID uuid) { + +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java new file mode 100644 index 00000000..4d7646d1 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacobject/RbacObject.java @@ -0,0 +1,8 @@ +package net.hostsharing.hsadminng.rbac.rbacobject; + + +import java.util.UUID; + +public interface RbacObject { + UUID getUuid(); +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java index ba251885..f29503c3 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java @@ -8,8 +8,8 @@ public interface RbacUserPermission { String getRoleName(); UUID getPermissionUuid(); String getOp(); + String getOpTableName(); String getObjectTable(); String getObjectIdName(); UUID getObjectUuid(); - } diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java index 1bd000ba..67607c83 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerController.java @@ -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> 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}") diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java index 1f2bb0e1..99b0fb3c 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -4,17 +4,27 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.persistence.HasUuid; +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 HasUuid { @Id @GeneratedValue @@ -25,4 +35,29 @@ 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") + // TODO: do we want explicit specification of parent-independent insert permissions? + // .toRole("global", ADMIN).grantPermission("customer", 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("113-test-customer-rbac"); + } } diff --git a/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java new file mode 100644 index 00000000..6a031df7 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/test/dom/TestDomainEntity.java @@ -0,0 +1,73 @@ +package net.hostsharing.hsadminng.test.dom; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import net.hostsharing.hsadminng.persistence.HasUuid; +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.Permission.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Role.*; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.SQL.fetchedBySql; +import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; + +@Entity +@Table(name = "test_domain_rv") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class TestDomainEntity implements HasUuid { + + @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"), + fetchedBySql(""" + SELECT * FROM test_package p + WHERE p.uuid= ${ref}.packageUuid + """)) + .toRole("package", ADMIN).grantPermission("domain", 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("133-test-domain-rbac"); + } +} 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 8687666f..757fcf05 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/pac/TestPackageEntity.java @@ -4,18 +4,28 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import net.hostsharing.hsadminng.persistence.HasUuid; +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.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 HasUuid { @Id @GeneratedValue @@ -31,4 +41,34 @@ 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"), + fetchedBySql(""" + SELECT * FROM test_customer c + WHERE c.uuid= ${ref}.customerUuid + """)) + .toRole("customer", ADMIN).grantPermission("package", 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("123-test-package-rbac"); + } } diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 4820cf9c..8de41891 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -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(96), + currentRequest text = null, + currentUser varchar(63) = null, + assumedRoles varchar(256) = null ) language plpgsql as $$ begin + currentTask := coalesce(currentTask, ''); + assert length(currentTask) <= 96, FORMAT('currentTask must not be longer than 96 characters: "%s"', currentTask); + assert length(currentTask) > 8, FORMAT('currentTask must be at least 8 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) <= 256, FORMAT('assumedRoles must not be longer than 256 characters: "%s"', assumedRoles); execute format('set local hsadminng.assumedRoles to %L', assumedRoles); call contextDefined(currentTask, currentRequest, currentUser, assumedRoles); diff --git a/src/main/resources/db/changelog/020-audit-log.sql b/src/main/resources/db/changelog/020-audit-log.sql index 173e5741..ec14ad0d 100644 --- a/src/main/resources/db/changelog/020-audit-log.sql +++ b/src/main/resources/db/changelog/020-audit-log.sql @@ -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 + assumedRoles varchar(256) not null, -- not the uuids, because roles can be deleted currentTask varchar(96) not null, - currentRequest varchar(512) not null + currentRequest text not null ); create index on tx_context using brin (txTimestamp); diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index fe2f30ae..2992d6a9 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -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; -$$; - --// -- ============================================================================ @@ -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) @@ -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,63 @@ 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, + uuid uuid primary key references RbacReference (uuid) on delete cascade, + objectUuid uuid not null references RbacObject, + op RbacOp not null, + opTableName varchar(60), unique (objectUuid, op) ); 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 = 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 +423,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 +443,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,23 +464,8 @@ 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) - returns uuid - returns null on null input - stable -- leakproof - language plpgsql as $$ -declare - permissionId uuid; -begin - permissionId := findPermissionId(forObjectUuid, forOp); - if permissionId is null and forOp <> '*' then - permissionId := findPermissionId(forObjectUuid, '*'); - end if; - return permissionId; -end $$; - --// -- ============================================================================ @@ -552,6 +562,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 +588,27 @@ select exists( ); $$; +create or replace procedure grantPermissionToRole(roleUuid uuid, permissionUuid 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(roleDesc RbacRoleDescriptor, permissionUuid uuid) + language plpgsql as $$ +begin + call grantPermissionToRole(findRoleId(roleDesc), permissionUuid); +end; +$$; + +-- TODO: deprecated, remove and use grantPermissionToRole(...) create or replace procedure grantPermissionsToRole(roleUuid uuid, permissionIds uuid[]) language plpgsql as $$ begin @@ -697,7 +740,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 +832,5 @@ do $$ create role restricted; grant all privileges on all tables in schema public to restricted; end if; - end $$ + end $$; --// - diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index 23dcbdd4..a82865c8 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -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; $$; +--// diff --git a/src/main/resources/db/changelog/055-rbac-views.sql b/src/main/resources/db/changelog/055-rbac-views.sql index b1757c56..b494d120 100644 --- a/src/main/resources/db/changelog/055-rbac-views.sql +++ b/src/main/resources/db/changelog/055-rbac-views.sql @@ -337,11 +337,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 @@ -357,11 +355,13 @@ 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.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 +373,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; +$$; --// diff --git a/src/main/resources/db/changelog/057-rbac-role-builder.sql b/src/main/resources/db/changelog/057-rbac-role-builder.sql index 81a81590..1a7da953 100644 --- a/src/main/resources/db/changelog/057-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/057-rbac-role-builder.sql @@ -13,24 +13,6 @@ 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 @@ -50,32 +32,37 @@ create or replace function createRoleWithGrants( language plpgsql as $$ declare roleUuid uuid; - superRoleUuid uuid; + subRoleDesc RbacRoleDescriptor; + superRoleDesc RbacRoleDescriptor; subRoleUuid uuid; + superRoleUuid uuid; userUuid uuid; grantedByRoleUuid uuid; begin roleUuid := createRole(roleDescriptor); - if cardinality(permissions) >0 then + if cardinality(permissions) > 0 then call grantPermissionsToRole(roleUuid, toPermissionUuids(roleDescriptor.objectuuid, permissions)); end if; - foreach superRoleUuid in array toRoleUuids(incomingSuperRoles) + foreach superRoleDesc in array array_remove(incomingSuperRoles, null) loop - call grantRoleToRole(roleUuid, superRoleUuid); + superRoleUuid := getRoleId(superRoleDesc); + call grantRoleToRole(roleUuid, superRoleUuid, superRoleDesc.assumed); end loop; - foreach subRoleUuid in array toRoleUuids(outgoingSubRoles) + foreach subRoleDesc in array array_remove(outgoingSubRoles, null) loop - call grantRoleToRole(subRoleUuid, roleUuid); + subRoleUuid := getRoleId(subRoleDesc); + call grantRoleToRole(subRoleUuid, roleUuid, subRoleDesc.assumed); 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'; + grantedByRoleUuid := roleUuid; + else + grantedByRoleUuid := getRoleId(grantedByRole); end if; - grantedByRoleUuid := getRoleId(grantedByRole, 'fail'); foreach userUuid in array userUuids loop call grantRoleToUserUnchecked(grantedByRoleUuid, roleUuid, userUuid); diff --git a/src/main/resources/db/changelog/058-rbac-generators.sql b/src/main/resources/db/changelog/058-rbac-generators.sql index fa198308..89d585ea 100644 --- a/src/main/resources/db/changelog/058-rbac-generators.sql +++ b/src/main/resources/db/changelog/058-rbac-generators.sql @@ -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,50 @@ 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) + 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$; $sql$, prefix, targetTable); @@ -92,7 +91,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 +100,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 +127,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; $$; --// @@ -145,13 +156,13 @@ begin targetTable := lower(targetTable); /* - 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 @@ -200,7 +211,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 +234,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 +242,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; diff --git a/src/main/resources/db/changelog/080-rbac-global.sql b/src/main/resources/db/changelog/080-rbac-global.sql index 034400fa..8313d05d 100644 --- a/src/main/resources/db/changelog/080-rbac-global.sql +++ b/src/main/resources/db/changelog/080-rbac-global.sql @@ -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,12 +109,12 @@ 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; diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.md b/src/main/resources/db/changelog/113-test-customer-rbac.md new file mode 100644 index 00000000..7770e470 --- /dev/null +++ b/src/main/resources/db/changelog/113-test-customer-rbac.md @@ -0,0 +1,43 @@ +### rbac customer + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.571772062. + +```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: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:customer:owner ==> perm:customer:DELETE +role:customer:admin ==> perm:customer:UPDATE +role:customer:tenant ==> perm:customer:SELECT + +``` diff --git a/src/main/resources/db/changelog/113-test-customer-rbac.sql b/src/main/resources/db/changelog/113-test-customer-rbac.sql index d7682cc1..6ae19710 100644 --- a/src/main/resources/db/changelog/113-test-customer-rbac.sql +++ b/src/main/resources/db/changelog/113-test-customer-rbac.sql @@ -1,4 +1,5 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.584886824. -- ============================================================================ --changeset test-customer-rbac-OBJECT:1 endDelimiter:--// @@ -15,82 +16,103 @@ call generateRbacRoleDescriptors('testCustomer', 'test_customer'); -- ============================================================================ ---changeset test-customer-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset test-customer-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* - Creates the roles and their assignments for a new customer for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission 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; +create or replace procedure buildRbacSystemForTestCustomer( + NEW test_customer +) + language plpgsql as $$ +declare + +begin call enterTriggerForObjectUuid(NEW.uuid); - -- the owner role with full access for Hostsharing administrators - testCustomerOwnerUuid = createRoleWithGrants( + perform createRoleWithGrants( testCustomerOwner(NEW), - permissions => array['*'], - incomingSuperRoles => array[globalAdmin()] - ); + permissions => array['DELETE'], + userUuids => array[currentUserUuid()], + incomingSuperRoles => array[globalAdmin(unassumed())] + ); - -- the admin role for the customer's admins, who can view and add products - customerAdminUuid = createRoleWithGrants( + perform 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() - ); + permissions => array['UPDATE'], + incomingSuperRoles => array[testCustomerOwner(NEW)] + ); - -- 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'] - ); + permissions => array['SELECT'], + incomingSuperRoles => array[testCustomerAdmin(NEW)] + ); call leaveTriggerForObjectUuid(NEW.uuid); - return NEW; end; $$; /* - An AFTER INSERT TRIGGER which creates the role structure for a new customer. + AFTER INSERT TRIGGER to create the role+grant structure for a new test_customer row. */ -drop trigger if exists createRbacRolesForTestCustomer_Trigger on test_customer; -create trigger createRbacRolesForTestCustomer_Trigger - after insert - on test_customer +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 createRbacRolesForTestCustomer(); +execute procedure insertTriggerForTestCustomer_tf(); + --// +-- ============================================================================ +--changeset test-customer-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/** + Checks if the user or assumed roles are allowed to insert a row to test_customer. +*/ +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 + -- As there is no explicit INSERT grant specified for this table, + -- only global admins are allowed to insert any rows. + when ( not isGlobalAdmin() ) + execute procedure test_customer_insert_permission_missing_tf(); + +--// -- ============================================================================ --changeset test-customer-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_customer', $idName$ - target.prefix + +call generateRbacIdentityViewFromProjection('test_customer', $idName$ + prefix $idName$); + --// - - -- ============================================================================ --changeset test-customer-rbac-RESTRICTED-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacRestrictedView('test_customer', 'target.prefix', +call generateRbacRestrictedView('test_customer', + 'reference', $updates$ reference = new.reference, prefix = new.prefix, @@ -99,47 +121,3 @@ call generateRbacRestrictedView('test_customer', 'target.prefix', --// --- ============================================================================ ---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(); ---// - diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 353b8f59..85c34ac6 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -28,6 +28,8 @@ 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'); @@ -35,10 +37,19 @@ begin 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; $$; --// diff --git a/src/main/resources/db/changelog/123-test-package-rbac.md b/src/main/resources/db/changelog/123-test-package-rbac.md new file mode 100644 index 00000000..78da4439 --- /dev/null +++ b/src/main/resources/db/changelog/123-test-package-rbac.md @@ -0,0 +1,59 @@ +### rbac package + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.624847792. + +```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 + +``` diff --git a/src/main/resources/db/changelog/123-test-package-rbac.sql b/src/main/resources/db/changelog/123-test-package-rbac.sql index 9e68468c..20562642 100644 --- a/src/main/resources/db/changelog/123-test-package-rbac.sql +++ b/src/main/resources/db/changelog/123-test-package-rbac.sql @@ -1,4 +1,5 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.625353859. -- ============================================================================ --changeset test-package-rbac-OBJECT:1 endDelimiter:--// @@ -15,95 +16,211 @@ call generateRbacRoleDescriptors('testPackage', 'test_package'); -- ============================================================================ ---changeset test-package-rbac-ROLES-CREATION:1 endDelimiter:--// +--changeset test-package-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- + /* - Creates the roles and their assignments for a new package for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRolesForTestPackage() - returns trigger - language plpgsql - strict as $$ + +create or replace procedure buildRbacSystemForTestPackage( + NEW test_package +) + language plpgsql as $$ + declare - parentCustomer test_customer; + newCustomer 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 c + WHERE c.uuid= NEW.customerUuid + into newCustomer; - 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)] - ); + testPackageOwner(NEW), + permissions => array['DELETE', 'UPDATE'], + incomingSuperRoles => array[testCustomerAdmin(newCustomer)] + ); - -- an owner role is created and assigned to the package owner role perform createRoleWithGrants( - testPackageAdmin(NEW), - permissions => array ['add-domain'], + testPackageAdmin(NEW), 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)] - ); + testPackageTenant(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[testPackageAdmin(NEW)], + outgoingSubRoles => array[testCustomerTenant(newCustomer)] + ); call leaveTriggerForObjectUuid(NEW.uuid); - return NEW; end; $$; /* - An AFTER INSERT TRIGGER which creates the role structure for a new package. + AFTER INSERT TRIGGER to create the role+grant structure for a new test_package row. */ -create trigger createRbacRolesForTestPackage_Trigger - after insert - on test_package +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 createRbacRolesForTestPackage(); +execute procedure insertTriggerForTestPackage_tf(); + --// - -- ============================================================================ ---changeset test-package-rbac-IDENTITY-VIEW:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_package', 'target.name'); ---// - - --- ============================================================================ ---changeset test-package-rbac-RESTRICTED-VIEW:1 endDelimiter:--// +--changeset test-package-rbac-update-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* - Creates a view to the customer main table which maps the identifying name - (in this case, the prefix) to the objectUuid. + Called from the AFTER UPDATE TRIGGER to re-wire the grants. */ --- 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$); +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 c + WHERE c.uuid= OLD.customerUuid + into oldCustomer; + SELECT * FROM test_customer c + WHERE c.uuid= NEW.customerUuid + into newCustomer; + + if NEW.customerUuid <> OLD.customerUuid then + + call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testCustomerAdmin(oldCustomer)); + + 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; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO test_package permissions for the related test_customer rows'); + + FOR row IN SELECT * FROM test_customer + LOOP + roleUuid := findRoleId(testCustomerAdmin(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', 'test_package'); + call grantPermissionToRole(roleUuid, permissionUuid); + 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( + testCustomerAdmin(NEW), + createPermission(NEW.uuid, 'INSERT', 'test_package')); + return NEW; +end; $$; + +create trigger 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. +*/ +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', + 'name', + $updates$ + version = new.version, + customerUuid = new.customerUuid, + description = new.description + $updates$); +--// + diff --git a/src/main/resources/db/changelog/128-test-package-test-data.sql b/src/main/resources/db/changelog/128-test-package-test-data.sql index 4667b742..9abba772 100644 --- a/src/main/resources/db/changelog/128-test-package-test-data.sql +++ b/src/main/resources/db/changelog/128-test-package-test-data.sql @@ -26,7 +26,7 @@ begin custAdminUser = 'customer-admin@' || cust.prefix || '.example.com'; custAdminRole = 'test_customer#' || cust.prefix || '.admin'; - call defineContext(currentTask, null, custAdminUser, custAdminRole); + call defineContext(currentTask, null, 'superuser-fran@hostsharing.net', custAdminRole); raise notice 'task: % by % as %', currentTask, custAdminUser, custAdminRole; insert @@ -35,7 +35,7 @@ begin returning * into pac; call grantRoleToUser( - getRoleId(testCustomerAdmin(cust), 'fail'), + getRoleId(testCustomerAdmin(cust)), findRoleId(testPackageAdmin(pac)), createRbacUser('pac-admin-' || pacName || '@' || cust.prefix || '.example.com'), true); diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.md b/src/main/resources/db/changelog/133-test-domain-rbac.md new file mode 100644 index 00000000..bd5cf706 --- /dev/null +++ b/src/main/resources/db/changelog/133-test-domain-rbac.md @@ -0,0 +1,88 @@ +### rbac domain + +This code generated was by RbacViewMermaidFlowchartGenerator at 2024-03-11T11:29:11.644658132. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph package.customer["`**package.customer**`"] + direction TB + style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer:roles[ ] + style package.customer:roles fill:#99bcdb,stroke:white + + role:package.customer:owner[[package.customer:owner]] + role:package.customer:admin[[package.customer:admin]] + role:package.customer:tenant[[package.customer:tenant]] + end +end + +subgraph package["`**package**`"] + direction TB + style package fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer["`**package.customer**`"] + direction TB + style package.customer fill:#99bcdb,stroke:#274d6e,stroke-width:8px + + subgraph package.customer:roles[ ] + style package.customer:roles fill:#99bcdb,stroke:white + + role:package.customer:owner[[package.customer:owner]] + role:package.customer:admin[[package.customer:admin]] + role:package.customer:tenant[[package.customer:tenant]] + end + end + + subgraph package:roles[ ] + style package:roles fill:#99bcdb,stroke:white + + role:package:owner[[package:owner]] + role:package:admin[[package:admin]] + role:package:tenant[[package:tenant]] + end +end + +subgraph domain["`**domain**`"] + direction TB + style domain fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph domain:roles[ ] + style domain:roles fill:#dd4901,stroke:white + + role:domain:owner[[domain:owner]] + role:domain:admin[[domain:admin]] + end + + subgraph domain:permissions[ ] + style domain:permissions fill:#dd4901,stroke:white + + perm:domain:INSERT{{domain:INSERT}} + perm:domain:DELETE{{domain:DELETE}} + perm:domain:UPDATE{{domain:UPDATE}} + perm:domain:SELECT{{domain:SELECT}} + end +end + +%% granting roles to roles +role:global:admin -.->|XX| role:package.customer:owner +role:package.customer:owner -.-> role:package.customer:admin +role:package.customer:admin -.-> role:package.customer:tenant +role:package.customer:admin -.-> role:package:owner +role:package:owner -.-> role:package:admin +role:package:admin -.-> role:package:tenant +role:package:tenant -.-> role:package.customer:tenant +role:package:admin ==> role:domain:owner +role:domain:owner ==> role:package:tenant +role:domain:owner ==> role:domain:admin +role:domain:admin ==> role:package:tenant + +%% granting permissions to roles +role:package:admin ==> perm:domain:INSERT +role:domain:owner ==> perm:domain:DELETE +role:domain:owner ==> perm:domain:UPDATE +role:domain:admin ==> perm:domain:SELECT + +``` diff --git a/src/main/resources/db/changelog/133-test-domain-rbac.sql b/src/main/resources/db/changelog/133-test-domain-rbac.sql index a78bfb5f..e686dada 100644 --- a/src/main/resources/db/changelog/133-test-domain-rbac.sql +++ b/src/main/resources/db/changelog/133-test-domain-rbac.sql @@ -1,4 +1,5 @@ --liquibase formatted sql +-- This code generated was by RbacViewPostgresGenerator at 2024-03-11T11:29:11.645391647. -- ============================================================================ --changeset test-domain-rbac-OBJECT:1 endDelimiter:--// @@ -11,107 +12,214 @@ 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:--// +--changeset test-domain-rbac-insert-trigger:1 endDelimiter:--// -- ---------------------------------------------------------------------------- + /* - Creates the roles and their assignments for a new domain for the AFTER INSERT TRIGGER. + Creates the roles, grants and permission for the AFTER INSERT TRIGGER. */ -create or replace function createRbacRulesForTestDomain() +create or replace procedure buildRbacSystemForTestDomain( + NEW test_domain +) + language plpgsql as $$ + +declare + newPackage test_package; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + SELECT * FROM test_package p + WHERE p.uuid= NEW.packageUuid + into newPackage; + + perform createRoleWithGrants( + testDomainOwner(NEW), + permissions => array['DELETE', 'UPDATE'], + incomingSuperRoles => array[testPackageAdmin(newPackage)], + outgoingSubRoles => array[testPackageTenant(newPackage)] + ); + + perform createRoleWithGrants( + testDomainAdmin(NEW), + permissions => array['SELECT'], + incomingSuperRoles => array[testDomainOwner(NEW)], + outgoingSubRoles => array[testPackageTenant(newPackage)] + ); + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to create the role+grant structure for a new test_domain row. + */ + +create or replace function insertTriggerForTestDomain_tf() 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); + call buildRbacSystemForTestDomain(NEW); 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 +create trigger insertTriggerForTestDomain_tg + after insert on test_domain for each row -execute procedure createRbacRulesForTestDomain(); +execute procedure insertTriggerForTestDomain_tf(); + --// +-- ============================================================================ +--changeset test-domain-rbac-update-trigger:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- +/* + Called from the AFTER UPDATE TRIGGER to re-wire the grants. + */ + +create or replace procedure updateRbacRulesForTestDomain( + OLD test_domain, + NEW test_domain +) + language plpgsql as $$ + +declare + oldPackage test_package; + newPackage test_package; + +begin + call enterTriggerForObjectUuid(NEW.uuid); + + SELECT * FROM test_package p + WHERE p.uuid= OLD.packageUuid + into oldPackage; + SELECT * FROM test_package p + WHERE p.uuid= NEW.packageUuid + into newPackage; + + if NEW.packageUuid <> OLD.packageUuid then + + call revokePermissionFromRole(findPermissionId(OLD.uuid, 'INSERT'), testPackageAdmin(oldPackage)); + + call revokeRoleFromRole(testDomainOwner(OLD), testPackageAdmin(oldPackage)); + call grantRoleToRole(testDomainOwner(NEW), testPackageAdmin(newPackage)); + + call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainOwner(OLD)); + call grantRoleToRole(testPackageTenant(newPackage), testDomainOwner(NEW)); + + call revokeRoleFromRole(testPackageTenant(oldPackage), testDomainAdmin(OLD)); + call grantRoleToRole(testPackageTenant(newPackage), testDomainAdmin(NEW)); + + end if; + + call leaveTriggerForObjectUuid(NEW.uuid); +end; $$; + +/* + AFTER INSERT TRIGGER to re-wire the grant structure for a new test_domain row. + */ + +create or replace function updateTriggerForTestDomain_tf() + returns trigger + language plpgsql + strict as $$ +begin + call updateRbacRulesForTestDomain(OLD, NEW); + return NEW; +end; $$; + +create trigger updateTriggerForTestDomain_tg + after update on test_domain + for each row +execute procedure updateTriggerForTestDomain_tf(); + +--// + +-- ============================================================================ +--changeset test-domain-rbac-INSERT:1 endDelimiter:--// +-- ---------------------------------------------------------------------------- + +/* + Creates INSERT INTO test_domain permissions for the related test_package rows. + */ +do language plpgsql $$ + declare + row test_package; + permissionUuid uuid; + roleUuid uuid; + begin + call defineContext('create INSERT INTO test_domain permissions for the related test_package rows'); + + FOR row IN SELECT * FROM test_package + LOOP + roleUuid := findRoleId(testPackageAdmin(row)); + permissionUuid := createPermission(row.uuid, 'INSERT', 'test_domain'); + call grantPermissionToRole(roleUuid, permissionUuid); + END LOOP; + END; +$$; + +/** + Adds test_domain INSERT permission to specified role of new test_package rows. +*/ +create or replace function test_domain_test_package_insert_tf() + returns trigger + language plpgsql + strict as $$ +begin + call grantPermissionToRole( + testPackageAdmin(NEW), + createPermission(NEW.uuid, 'INSERT', 'test_domain')); + return NEW; +end; $$; + +create trigger test_domain_test_package_insert_tg + after insert on test_package + for each row +execute procedure test_domain_test_package_insert_tf(); + +/** + Checks if the user or assumed roles are allowed to insert a row to test_domain. +*/ +create or replace function test_domain_insert_permission_missing_tf() + returns trigger + language plpgsql as $$ +begin + raise exception '[403] insert into test_domain not allowed for current subjects % (%)', + currentSubjects(), currentSubjectsUuids(); +end; $$; + +create trigger test_domain_insert_permission_check_tg + before insert on test_domain + for each row + when ( not hasInsertPermission(NEW.packageUuid, 'INSERT', 'test_domain') ) + execute procedure test_domain_insert_permission_missing_tf(); + +--// -- ============================================================================ --changeset test-domain-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('test_domain', $idName$ - target.name + +call generateRbacIdentityViewFromProjection('test_domain', $idName$ + 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}; +call generateRbacRestrictedView('test_domain', + 'name', + $updates$ + version = new.version, + packageUuid = new.packageUuid, + description = new.description + $updates$); --// + + diff --git a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql index 7ba7891b..3a9b0c34 100644 --- a/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql +++ b/src/main/resources/db/changelog/203-hs-office-contact-rbac.sql @@ -33,7 +33,7 @@ begin perform createRoleWithGrants( hsOfficeContactOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -41,7 +41,7 @@ begin perform createRoleWithGrants( hsOfficeContactAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeContactOwner(NEW)] ); @@ -52,7 +52,7 @@ begin perform createRoleWithGrants( hsOfficeContactGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeContactTenant(NEW)] ); @@ -75,7 +75,7 @@ execute procedure createRbacRolesForHsOfficeContact(); --changeset hs-office-contact-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_contact', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_contact', $idName$ target.label $idName$); --// diff --git a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql index 42eacf2f..fbb1f8e1 100644 --- a/src/main/resources/db/changelog/213-hs-office-person-rbac.sql +++ b/src/main/resources/db/changelog/213-hs-office-person-rbac.sql @@ -31,16 +31,16 @@ begin perform createRoleWithGrants( hsOfficePersonOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() ); - -- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to edit the data? + -- TODO: who is admin? the person itself? is it allowed for the person itself or a representative to update the data? perform createRoleWithGrants( hsOfficePersonAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficePersonOwner(NEW)] ); @@ -51,7 +51,7 @@ begin perform createRoleWithGrants( hsOfficePersonGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficePersonTenant(NEW)] ); @@ -73,7 +73,7 @@ execute procedure createRbacRolesForHsOfficePerson(); -- ============================================================================ --changeset hs-office-person-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_person', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_person', $idName$ concat(target.tradeName, target.familyName, target.givenName) $idName$); --// diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md index c41de32c..8ffa55ff 100644 --- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md +++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.md @@ -42,151 +42,3 @@ subgraph hsOfficeRelationship end ``` - if TG_OP = 'INSERT' then - - -- the owner role with full access for admins of the relAnchor global admins - ownerRole = createRole( - hsOfficeRelationshipOwner(NEW), - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['*']), - beneathRoles(array[ - globalAdmin(), - hsOfficePersonAdmin(newRelAnchor)]) - ); - - -- the admin role with full access for the owner - adminRole = createRole( - hsOfficeRelationshipAdmin(NEW), - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['edit']), - beneathRole(ownerRole) - ); - - -- the tenant role for those related users who can view the data - perform createRole( - hsOfficeRelationshipTenant, - grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view']), - beneathRoles(array[ - hsOfficePersonAdmin(newRelAnchor), - hsOfficePersonAdmin(newRelHolder), - hsOfficeContactAdmin(newContact)]), - withSubRoles(array[ - hsOfficePersonTenant(newRelAnchor), - hsOfficePersonTenant(newRelHolder), - hsOfficeContactTenant(newContact)]) - ); - - -- anchor and holder admin roles need each others tenant role - -- to be able to see the joined relationship - call grantRoleToRole(hsOfficePersonTenant(newRelAnchor), hsOfficePersonAdmin(newRelHolder)); - call grantRoleToRole(hsOfficePersonTenant(newRelHolder), hsOfficePersonAdmin(newRelAnchor)); - call grantRoleToRoleIfNotNull(hsOfficePersonTenant(newRelHolder), hsOfficeContactAdmin(newContact)); - - elsif TG_OP = 'UPDATE' then - - if OLD.contactUuid <> NEW.contactUuid then - -- nothing but the contact can be updated, - -- in other cases, a new relationship needs to be created and the old updated - - select * from hs_office_contact as c where c.uuid = OLD.contactUuid into oldContact; - - call revokeRoleFromRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(oldContact) ); - call grantRoleToRole( hsOfficeRelationshipTenant, hsOfficeContactAdmin(newContact) ); - - call revokeRoleFromRole( hsOfficeContactTenant(oldContact), hsOfficeRelationshipTenant ); - call grantRoleToRole( hsOfficeContactTenant(newContact), hsOfficeRelationshipTenant ); - end if; - else - raise exception 'invalid usage of TRIGGER'; - end if; - - return NEW; -end; $$; - -/* - An AFTER INSERT TRIGGER which creates the role structure for a new customer. - */ -create trigger createRbacRolesForHsOfficeRelationship_Trigger - after insert - on hs_office_relationship - for each row -execute procedure hsOfficeRelationshipRbacRolesTrigger(); - -/* - An AFTER UPDATE TRIGGER which updates the role structure of a customer. - */ -create trigger updateRbacRolesForHsOfficeRelationship_Trigger - after update - on hs_office_relationship - for each row -execute procedure hsOfficeRelationshipRbacRolesTrigger(); ---// - - --- ============================================================================ ---changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_relationship', $idName$ - (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) - || '-with-' || target.relType || '-' || - (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) - $idName$); ---// - - --- ============================================================================ ---changeset hs-office-relationship-rbac-RESTRICTED-VIEW:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -call generateRbacRestrictedView('hs_office_relationship', - '(select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid)', - $updates$ - contactUuid = new.contactUuid - $updates$); ---// - --- TODO: exception if one tries to amend any other column - - --- ============================================================================ ---changeset hs-office-relationship-rbac-NEW-RELATHIONSHIP:1 endDelimiter:--// --- ---------------------------------------------------------------------------- -/* - Creates a global permission for new-relationship and assigns it to the hostsharing admins role. - */ -do language plpgsql $$ - declare - addCustomerPermissions uuid[]; - globalObjectUuid uuid; - globalAdminRoleUuid uuid ; - begin - call defineContext('granting global new-relationship permission to global admin role', null, null, null); - - globalAdminRoleUuid := findRoleId(globalAdmin()); - globalObjectUuid := (select uuid from global); - addCustomerPermissions := createPermissions(globalObjectUuid, array ['new-relationship']); - call grantPermissionsToRole(globalAdminRoleUuid, addCustomerPermissions); - end; -$$; - -/** - Used by the trigger to prevent the add-customer to current user respectively assumed roles. - */ -create or replace function addHsOfficeRelationshipNotAllowedForCurrentSubjects() - returns trigger - language PLPGSQL -as $$ -begin - raise exception '[403] new-relationship not permitted for %', - array_to_string(currentSubjects(), ';', 'null'); -end; $$; - -/** - Checks if the user or assumed roles are allowed to create a new customer. - */ -create trigger hs_office_relationship_insert_trigger - before insert - on hs_office_relationship - for each row - -- TODO.spec: who is allowed to create new relationships - when ( not hasAssumedRole() ) -execute procedure addHsOfficeRelationshipNotAllowedForCurrentSubjects(); ---// - diff --git a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql index 928af48c..126664a4 100644 --- a/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql +++ b/src/main/resources/db/changelog/223-hs-office-relationship-rbac.sql @@ -45,7 +45,7 @@ begin perform createRoleWithGrants( hsOfficeRelationshipOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[ globalAdmin(), hsOfficePersonAdmin(newRelAnchor)] @@ -53,14 +53,14 @@ begin perform createRoleWithGrants( hsOfficeRelationshipAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeRelationshipOwner(NEW)] ); -- the tenant role for those related users who can view the data perform createRoleWithGrants( hsOfficeRelationshipTenant, - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[ hsOfficeRelationshipAdmin(NEW), hsOfficePersonAdmin(newRelAnchor), @@ -124,7 +124,7 @@ execute procedure hsOfficeRelationshipRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-relationship-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_relationship', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_relationship', $idName$ (select idName from hs_office_person_iv p where p.uuid = target.relAnchorUuid) || '-with-' || target.relType || '-' || (select idName from hs_office_person_iv p where p.uuid = target.relHolderUuid) diff --git a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql index 4b4da009..d16048fd 100644 --- a/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql +++ b/src/main/resources/db/changelog/233-hs-office-partner-rbac.sql @@ -48,13 +48,13 @@ begin perform createRoleWithGrants( hsOfficePartnerOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficePartnerAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[ hsOfficePartnerOwner(NEW)], outgoingSubRoles => array[ @@ -84,7 +84,7 @@ begin perform createRoleWithGrants( hsOfficePartnerGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficePartnerTenant(NEW)] ); @@ -98,21 +98,21 @@ begin --Attention: Cannot be in partner-details because of insert order (partner is not in database yet) call grantPermissionsToRole( - getRoleId(hsOfficePartnerOwner(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['*']) + getRoleId(hsOfficePartnerOwner(NEW)), + createPermissions(NEW.detailsUuid, array ['DELETE']) ); call grantPermissionsToRole( - getRoleId(hsOfficePartnerAdmin(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['edit']) + getRoleId(hsOfficePartnerAdmin(NEW)), + createPermissions(NEW.detailsUuid, array ['UPDATE']) ); call grantPermissionsToRole( -- Yes, here hsOfficePartnerAGENT is used, not hsOfficePartnerTENANT. -- Do NOT grant view permission on partner-details to hsOfficePartnerTENANT! -- Otherwise package-admins etc. would be able to read the data. - getRoleId(hsOfficePartnerAgent(NEW), 'fail'), - createPermissions(NEW.detailsUuid, array ['view']) + getRoleId(hsOfficePartnerAgent(NEW)), + createPermissions(NEW.detailsUuid, array ['SELECT']) ); @@ -187,7 +187,7 @@ execute procedure hsOfficePartnerRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-partner-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_partner', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_partner', $idName$ partnerNumber || ':' || (select idName from hs_office_person_iv p where p.uuid = target.personuuid) || '-' || diff --git a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql index ab94481e..c4e053b9 100644 --- a/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql +++ b/src/main/resources/db/changelog/234-hs-office-partner-details-rbac.sql @@ -7,13 +7,10 @@ call generateRelatedRbacObject('hs_office_partner_details'); --// - - - -- ============================================================================ --changeset hs-office-partner-details-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_partner_details', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_partner_details', $idName$ (select idName || '-details' from hs_office_partner_iv partner_iv join hs_office_partner partner on (partner_iv.uuid = partner.uuid) where partner.detailsUuid = target.uuid) @@ -38,7 +35,7 @@ call generateRbacRestrictedView('hs_office_partner_details', -- ============================================================================ ---changeset hs-office-partner-details-rbac-NEW-CONTACT:1 endDelimiter:--// +--changeset hs-office-partner-details-rbac-NEW-PARTNER-DETAILS:1 endDelimiter:--// -- ---------------------------------------------------------------------------- /* Creates a global permission for new-partner-details and assigns it to the hostsharing admins role. diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md index fc34f147..b2cee782 100644 --- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.md @@ -4,14 +4,14 @@ flowchart TB subgraph global - style hsOfficeBankAccount fill: #e9f7ef + style global fill: lightgray role:global.admin[global.admin] end subgraph hsOfficeBankAccount direction TB - style hsOfficeBankAccount fill: #e9f7ef + style hsOfficeBankAccount fill: lightgreen user:hsOfficeBankAccount.creator([bankAccount.creator]) diff --git a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql index 148e0ee2..93b605ce 100644 --- a/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql +++ b/src/main/resources/db/changelog/243-hs-office-bankaccount-rbac.sql @@ -33,7 +33,7 @@ begin perform createRoleWithGrants( hsOfficeBankAccountOwner(NEW), - permissions => array['delete'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -51,7 +51,7 @@ begin perform createRoleWithGrants( hsOfficeBankAccountGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeBankAccountTenant(NEW)] ); @@ -74,7 +74,7 @@ execute procedure createRbacRolesForHsOfficeBankAccount(); --changeset hs-office-bankaccount-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_bankaccount', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_bankaccount', $idName$ target.holder $idName$); --// diff --git a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql index 02895c48..da7887cd 100644 --- a/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql +++ b/src/main/resources/db/changelog/253-hs-office-sepamandate-rbac.sql @@ -41,13 +41,13 @@ begin perform createRoleWithGrants( hsOfficeSepaMandateOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficeSepaMandateAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeSepaMandateOwner(NEW)], outgoingSubRoles => array[hsOfficeBankAccountTenant(newHsOfficeBankAccount)] ); @@ -66,7 +66,7 @@ begin perform createRoleWithGrants( hsOfficeSepaMandateGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeSepaMandateTenant(NEW)] ); @@ -94,7 +94,7 @@ execute procedure hsOfficeSepaMandateRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-sepamandate-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_sepamandate', idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_sepamandate', 'target.reference'); --// diff --git a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql index 30573125..5f684f49 100644 --- a/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql +++ b/src/main/resources/db/changelog/273-hs-office-debitor-rbac.sql @@ -49,7 +49,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()], userUuids => array[currentUserUuid()], grantedByRole => globalAdmin() @@ -57,7 +57,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeDebitorOwner(NEW)] ); @@ -85,7 +85,7 @@ begin perform createRoleWithGrants( hsOfficeDebitorGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[ hsOfficeDebitorTenant(NEW)] ); @@ -173,7 +173,7 @@ execute procedure hsOfficeDebitorRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-debitor-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_debitor', $idName$ +call generateRbacIdentityViewFromProjection('hs_office_debitor', $idName$ '#' || (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) || to_char(debitorNumberSuffix, 'fm00') || diff --git a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql index 949f939c..2a4a4a50 100644 --- a/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql +++ b/src/main/resources/db/changelog/303-hs-office-membership-rbac.sql @@ -41,13 +41,13 @@ begin perform createRoleWithGrants( hsOfficeMembershipOwner(NEW), - permissions => array['*'], + permissions => array['DELETE'], incomingSuperRoles => array[globalAdmin()] ); perform createRoleWithGrants( hsOfficeMembershipAdmin(NEW), - permissions => array['edit'], + permissions => array['UPDATE'], incomingSuperRoles => array[hsOfficeMembershipOwner(NEW)] ); @@ -65,7 +65,7 @@ begin perform createRoleWithGrants( hsOfficeMembershipGuest(NEW), - permissions => array['view'], + permissions => array['SELECT'], incomingSuperRoles => array[hsOfficeMembershipTenant(NEW), hsOfficePartnerTenant(newHsOfficePartner), hsOfficeDebitorTenant(newHsOfficeDebitor)] ); @@ -93,7 +93,7 @@ execute procedure hsOfficeMembershipRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-membership-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_membership', idNameExpression => $idName$ +call generateRbacIdentityViewFromProjection('hs_office_membership', $idName$ '#' || (select partnerNumber from hs_office_partner p where p.uuid = target.partnerUuid) || memberNumberSuffix || diff --git a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql index dd465d9f..5ee8bfbe 100644 --- a/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql +++ b/src/main/resources/db/changelog/313-hs-office-coopshares-rbac.sql @@ -42,8 +42,8 @@ begin -- coopsharestransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( - getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), - createPermissions(NEW.uuid, array ['view']) + getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)), + createPermissions(NEW.uuid, array ['SELECT']) ); else @@ -68,8 +68,7 @@ execute procedure hsOfficeCoopSharesTransactionRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-coopSharesTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_coopSharesTransaction', - idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_coopSharesTransaction', 'target.reference'); --// diff --git a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql index ac65c141..69920385 100644 --- a/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql +++ b/src/main/resources/db/changelog/323-hs-office-coopassets-rbac.sql @@ -42,8 +42,8 @@ begin -- coopassetstransactions cannot be edited nor deleted, just created+viewed call grantPermissionsToRole( - getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership), 'fail'), - createPermissions(NEW.uuid, array ['view']) + getRoleId(hsOfficeMembershipTenant(newHsOfficeMembership)), + createPermissions(NEW.uuid, array ['SELECT']) ); else @@ -68,8 +68,7 @@ execute procedure hsOfficeCoopAssetsTransactionRbacRolesTrigger(); -- ============================================================================ --changeset hs-office-coopAssetsTransaction-rbac-IDENTITY-VIEW:1 endDelimiter:--// -- ---------------------------------------------------------------------------- -call generateRbacIdentityView('hs_office_coopAssetsTransaction', - idNameExpression => 'target.reference'); +call generateRbacIdentityViewFromProjection('hs_office_coopAssetsTransaction', 'target.reference'); --// diff --git a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java index fe50ccf1..013b2309 100644 --- a/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java +++ b/src/test/java/net/hostsharing/hsadminng/arch/ArchitectureTest.java @@ -28,6 +28,7 @@ public class ArchitectureTest { "..test", "..test.cust", "..test.pac", + "..test.dom", "..context", "..generated..", "..persistence..", @@ -49,6 +50,8 @@ public class ArchitectureTest { "..rbac.rbacuser", "..rbac.rbacgrant", "..rbac.rbacrole", + "..rbac.rbacobject", + "..rbac.rbacdef", "..stringify" // ATTENTION: Don't simply add packages here, also add arch rules for the new package! ); @@ -116,7 +119,10 @@ public class ArchitectureTest { public static final ArchRule hsAdminPackagesRule = classes() .that().resideInAPackage("..hs.office.(*)..") .should().onlyBeAccessed().byClassesThat() - .resideInAnyPackage("..hs.office.(*).."); + .resideInAnyPackage( + "..hs.office.(*)..", + "..rbac.rbacgrant" // TODO: just because of RbacGrantsDiagramServiceIntegrationTest + ); @ArchTest @SuppressWarnings("unused") diff --git a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java index 1069fa5f..7f08f044 100644 --- a/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java +++ b/src/test/java/net/hostsharing/hsadminng/context/ContextBasedTest.java @@ -1,14 +1,37 @@ package net.hostsharing.hsadminng.context; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Import; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +@Import(RbacGrantsDiagramService.class) public abstract class ContextBasedTest { @Autowired protected Context context; + @PersistenceContext + protected EntityManager em; // just to be used in subclasses + + /** + * To generate a flowchart diagram from the database use something like this in a defined context: + +
+     RbacGrantsDiagramService.writeToFile(
+         "title",
+         diagramService.allGrantsToCurrentUser(of(RbacGrantsDiagramService.Include.USERS, RbacGrantsDiagramService.Include.TEST_ENTITIES, RbacGrantsDiagramService.Include.NOT_ASSUMED, RbacGrantsDiagramService.Include.DETAILS, RbacGrantsDiagramService.Include.PERMISSIONS)),
+         "filename.md
+     );
+    
+ */ + @Autowired + protected RbacGrantsDiagramService diagramService; // just to be used in subclasses + TestInfo test; @BeforeEach diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java index f2847290..eb14e634 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountRepositoryIntegrationTest.java @@ -109,7 +109,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC )); assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm delete on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", + "{ grant perm DELETE on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.owner by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.owner to role global#global.admin by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }", @@ -117,7 +117,7 @@ class HsOfficeBankAccountRepositoryIntegrationTest extends ContextBasedTestWithC "{ grant role hs_office_bankaccount#sometempaccC.tenant to role hs_office_bankaccount#sometempaccC.admin by system and assume }", - "{ grant perm view on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.guest by system and assume }", + "{ grant perm SELECT on hs_office_bankaccount#sometempaccC to role hs_office_bankaccount#sometempaccC.guest by system and assume }", "{ grant role hs_office_bankaccount#sometempaccC.guest to role hs_office_bankaccount#sometempaccC.tenant by system and assume }", null )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java index a78b761e..91ee8bde 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/contact/HsOfficeContactRepositoryIntegrationTest.java @@ -111,11 +111,11 @@ class HsOfficeContactRepositoryIntegrationTest extends ContextBasedTestWithClean assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.from( initialGrantNames, "{ grant role hs_office_contact#anothernewcontact.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.admin by system and assume }", + "{ grant perm UPDATE on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.admin by system and assume }", "{ grant role hs_office_contact#anothernewcontact.tenant to role hs_office_contact#anothernewcontact.admin by system and assume }", - "{ grant perm * on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.owner by system and assume }", + "{ grant perm DELETE on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.owner by system and assume }", "{ grant role hs_office_contact#anothernewcontact.admin to role hs_office_contact#anothernewcontact.owner by system and assume }", - "{ grant perm view on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.guest by system and assume }", + "{ grant perm SELECT on hs_office_contact#anothernewcontact to role hs_office_contact#anothernewcontact.guest by system and assume }", "{ grant role hs_office_contact#anothernewcontact.guest to role hs_office_contact#anothernewcontact.tenant by system and assume }", "{ grant role hs_office_contact#anothernewcontact.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java index f18447df..1f6964b8 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopassets/HsOfficeCoopAssetsTransactionRepositoryIntegrationTest.java @@ -114,7 +114,7 @@ class HsOfficeCoopAssetsTransactionRepositoryIntegrationTest extends ContextBase .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopassetstransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm SELECT on coopassetstransaction#temprefB to role membership#1000101:....tenant by system and assume }", null)); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java index 20602661..609e7940 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/coopshares/HsOfficeCoopSharesTransactionRepositoryIntegrationTest.java @@ -113,7 +113,7 @@ class HsOfficeCoopSharesTransactionRepositoryIntegrationTest extends ContextBase .map(s -> s.replace("hs_office_", "")) .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm view on coopsharestransaction#temprefB to role membership#1000101:....tenant by system and assume }", + "{ grant perm SELECT on coopsharestransaction#temprefB to role membership#1000101:....tenant by system and assume }", null)); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java index 839039a2..0616e338 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorControllerAcceptanceTest.java @@ -145,8 +145,7 @@ class HsOfficeDebitorControllerAcceptanceTest extends ContextBasedTestWithCleanu } @Nested - @Accepts({ "Debitor:C(Create)" }) - class CreateDebitor { + class AddDebitor { @Test void globalAdmin_withoutAssumedRole_canAddDebitorWithBankAccount() { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java index c703c31a..46d0878f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/debitor/HsOfficeDebitorRepositoryIntegrationTest.java @@ -118,8 +118,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean }); // then - System.out.println("ok"); -// result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); + result.assertExceptionWithRootCauseMessage(org.hibernate.exception.ConstraintViolationException.class); } @Test @@ -167,12 +166,12 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean .containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, // owner - "{ grant perm * on debitor#1000422:FeG to role debitor#1000422:FeG.owner by system and assume }", + "{ grant perm DELETE on debitor#1000422:FeG to role debitor#1000422:FeG.owner by system and assume }", "{ grant role debitor#1000422:FeG.owner to role global#global.admin by system and assume }", "{ grant role debitor#1000422:FeG.owner to user superuser-alex by global#global.admin and assume }", // admin - "{ grant perm edit on debitor#1000422:FeG to role debitor#1000422:FeG.admin by system and assume }", + "{ grant perm UPDATE on debitor#1000422:FeG to role debitor#1000422:FeG.admin by system and assume }", "{ grant role debitor#1000422:FeG.admin to role debitor#1000422:FeG.owner by system and assume }", // agent @@ -187,7 +186,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role partner#10004:FeG.tenant to role debitor#1000422:FeG.tenant by system and assume }", // guest - "{ grant perm view on debitor#1000422:FeG to role debitor#1000422:FeG.guest by system and assume }", + "{ grant perm SELECT on debitor#1000422:FeG to role debitor#1000422:FeG.guest by system and assume }", "{ grant role debitor#1000422:FeG.guest to role debitor#1000422:FeG.tenant by system and assume }", null)); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java index 6a0cd485..4483304a 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/membership/HsOfficeMembershipRepositoryIntegrationTest.java @@ -126,11 +126,11 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl initialGrantNames, // owner - "{ grant perm * on membership#1000117:First to role membership#1000117:First.owner by system and assume }", + "{ grant perm DELETE on membership#1000117:First to role membership#1000117:First.owner by system and assume }", "{ grant role membership#1000117:First.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on membership#1000117:First to role membership#1000117:First.admin by system and assume }", + "{ grant perm UPDATE on membership#1000117:First to role membership#1000117:First.admin by system and assume }", "{ grant role membership#1000117:First.admin to role membership#1000117:First.owner by system and assume }", // agent @@ -149,7 +149,7 @@ class HsOfficeMembershipRepositoryIntegrationTest extends ContextBasedTestWithCl "{ grant role membership#1000117:First.tenant to role partner#10001:First.agent by system and assume }", // guest - "{ grant perm view on membership#1000117:First to role membership#1000117:First.guest by system and assume }", + "{ grant perm SELECT on membership#1000117:First to role membership#1000117:First.guest by system and assume }", "{ grant role membership#1000117:First.guest to role membership#1000117:First.tenant by system and assume }", "{ grant role membership#1000117:First.guest to role partner#10001:First.tenant by system and assume }", "{ grant role membership#1000117:First.guest to role debitor#1000111:First.tenant by system and assume }", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java index 325317b2..929aa919 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/migration/ImportOfficeData.java @@ -21,7 +21,7 @@ import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipEntity; import net.hostsharing.hsadminng.hs.office.relationship.HsOfficeRelationshipType; import net.hostsharing.hsadminng.hs.office.sepamandate.HsOfficeSepaMandateEntity; -import net.hostsharing.hsadminng.persistence.HasUuid; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.test.JpaAttempt; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; @@ -520,7 +520,7 @@ public class ImportOfficeData extends ContextBasedTest { } - private void persist(final Integer id, final HasUuid entity) { + private void persist(final Integer id, final RbacObject entity) { try { //System.out.println("persisting #" + entity.hashCode() + ": " + entity); em.persist(entity); @@ -591,7 +591,7 @@ public class ImportOfficeData extends ContextBasedTest { }).assertSuccessful(); } - private void updateLegacyIds( + private void updateLegacyIds( Map entities, final String legacyIdTable, final String legacyIdColumn) { diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java index 2512a07d..94d06a77 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/partner/HsOfficePartnerRepositoryIntegrationTest.java @@ -171,29 +171,29 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#EBess.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.owner to role person#HostsharingeG.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role person#HostsharingeG.admin by system and assume }", - "{ grant perm edit on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", + "{ grant perm UPDATE on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.admin by system and assume }", - "{ grant perm * on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", + "{ grant perm DELETE on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", "{ grant role relationship#HostsharingeG-with-PARTNER-EBess.admin to role relationship#HostsharingeG-with-PARTNER-EBess.owner by system and assume }", - "{ grant perm view on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", + "{ grant perm SELECT on relationship#HostsharingeG-with-PARTNER-EBess to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role contact#4th.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role person#EBess.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", "{ grant role person#HostsharingeG.tenant to role relationship#HostsharingeG-with-PARTNER-EBess.tenant by system and assume }", // owner - "{ grant perm * on partner#20032:EBess-4th to role partner#20032:EBess-4th.owner by system and assume }", - "{ grant perm * on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.owner by system and assume }", + "{ grant perm DELETE on partner#20032:EBess-4th to role partner#20032:EBess-4th.owner by system and assume }", + "{ grant perm DELETE on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.owner by system and assume }", "{ grant role partner#20032:EBess-4th.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on partner#20032:EBess-4th to role partner#20032:EBess-4th.admin by system and assume }", - "{ grant perm edit on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.admin by system and assume }", + "{ grant perm UPDATE on partner#20032:EBess-4th to role partner#20032:EBess-4th.admin by system and assume }", + "{ grant perm UPDATE on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role partner#20032:EBess-4th.admin to role partner#20032:EBess-4th.owner by system and assume }", "{ grant role person#EBess.tenant to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role contact#4th.tenant to role partner#20032:EBess-4th.admin by system and assume }", // agent - "{ grant perm view on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.agent by system and assume }", + "{ grant perm SELECT on partner_details#20032:EBess-4th-details to role partner#20032:EBess-4th.agent by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role partner#20032:EBess-4th.admin by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role person#EBess.admin by system and assume }", "{ grant role partner#20032:EBess-4th.agent to role contact#4th.admin by system and assume }", @@ -204,7 +204,7 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean "{ grant role contact#4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", // guest - "{ grant perm view on partner#20032:EBess-4th to role partner#20032:EBess-4th.guest by system and assume }", + "{ grant perm SELECT on partner#20032:EBess-4th to role partner#20032:EBess-4th.guest by system and assume }", "{ grant role partner#20032:EBess-4th.guest to role partner#20032:EBess-4th.tenant by system and assume }", null))); @@ -473,7 +473,6 @@ class HsOfficePartnerRepositoryIntegrationTest extends ContextBasedTestWithClean .contact(givenContact) .build(); relationshipRepo.save(partnerRole); - em.flush(); // TODO: why is that necessary? final var newPartner = HsOfficePartnerEntity.builder() .partnerNumber(partnerNumber) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java index dd3e08c9..d3da9ada 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/person/HsOfficePersonRepositoryIntegrationTest.java @@ -113,11 +113,11 @@ class HsOfficePersonRepositoryIntegrationTest extends ContextBasedTestWithCleanu Array.from( initialGrantNames, "{ grant role hs_office_person#anothernewperson.owner to role global#global.admin by system and assume }", - "{ grant perm edit on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.admin by system and assume }", + "{ grant perm UPDATE on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.admin by system and assume }", "{ grant role hs_office_person#anothernewperson.tenant to role hs_office_person#anothernewperson.admin by system and assume }", - "{ grant perm * on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.owner by system and assume }", + "{ grant perm DELETE on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.owner by system and assume }", "{ grant role hs_office_person#anothernewperson.admin to role hs_office_person#anothernewperson.owner by system and assume }", - "{ grant perm view on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.guest by system and assume }", + "{ grant perm SELECT on hs_office_person#anothernewperson to role hs_office_person#anothernewperson.guest by system and assume }", "{ grant role hs_office_person#anothernewperson.guest to role hs_office_person#anothernewperson.tenant by system and assume }", "{ grant role hs_office_person#anothernewperson.owner to user selfregistered-user-drew@hostsharing.org by global#global.admin and assume }" )); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java index 8d89479c..46d60a40 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/relationship/HsOfficeRelationshipRepositoryIntegrationTest.java @@ -115,14 +115,14 @@ class HsOfficeRelationshipRepositoryIntegrationTest extends ContextBasedTestWith assertThat(distinctGrantDisplaysOf(rawGrantRepo.findAll())).containsExactlyInAnyOrder(Array.fromFormatted( initialGrantNames, - "{ grant perm * on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", + "{ grant perm DELETE on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role global#global.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner to role hs_office_person#BesslerAnita.admin by system and assume }", - "{ grant perm edit on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", + "{ grant perm UPDATE on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.admin to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.owner by system and assume }", - "{ grant perm view on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", + "{ grant perm SELECT on hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita to role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_contact#fourthcontact.admin by system and assume }", "{ grant role hs_office_relationship#BesslerAnita-with-REPRESENTATIVE-BesslerAnita.tenant to role hs_office_person#BesslerAnita.admin by system and assume }", diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java index 04b5b5cf..79910d28 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/sepamandate/HsOfficeSepaMandateRepositoryIntegrationTest.java @@ -131,11 +131,11 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC initialGrantNames, // owner - "{ grant perm * on sepamandate#temprefB to role sepamandate#temprefB.owner by system and assume }", + "{ grant perm DELETE on sepamandate#temprefB to role sepamandate#temprefB.owner by system and assume }", "{ grant role sepamandate#temprefB.owner to role global#global.admin by system and assume }", // admin - "{ grant perm edit on sepamandate#temprefB to role sepamandate#temprefB.admin by system and assume }", + "{ grant perm UPDATE on sepamandate#temprefB to role sepamandate#temprefB.admin by system and assume }", "{ grant role sepamandate#temprefB.admin to role sepamandate#temprefB.owner by system and assume }", "{ grant role bankaccount#Paul....tenant to role sepamandate#temprefB.admin by system and assume }", @@ -151,7 +151,7 @@ class HsOfficeSepaMandateRepositoryIntegrationTest extends ContextBasedTestWithC "{ grant role bankaccount#Paul....guest to role sepamandate#temprefB.tenant by system and assume }", // guest - "{ grant perm view on sepamandate#temprefB to role sepamandate#temprefB.guest by system and assume }", + "{ grant perm SELECT on sepamandate#temprefB to role sepamandate#temprefB.guest by system and assume }", "{ grant role sepamandate#temprefB.guest to role sepamandate#temprefB.tenant by system and assume }", null)); } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java index 9b6c14ed..968e5416 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/test/ContextBasedTestWithCleanup.java @@ -4,6 +4,7 @@ import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.hsadminng.persistence.HasUuid; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantEntity; import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantRepository; +import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.test.JpaAttempt; @@ -43,7 +44,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { @Autowired JpaAttempt jpaAttempt; - private TreeMap> entitiesToCleanup = new TreeMap<>(); + private TreeMap> entitiesToCleanup = new TreeMap<>(); private static Long latestIntialTestDataSerialId; private static boolean countersInitialized = false; @@ -61,7 +62,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest { return uuidToCleanup; } - public E toCleanup(final E entity) { + public E toCleanup(final E entity) { out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid()); if ( entity.getUuid() == null ) { throw new IllegalArgumentException("only persisted entities with valid uuid allowed"); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java index 6f0abc93..f56baf34 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantControllerAcceptanceTest.java @@ -73,14 +73,16 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { .contentType("application/json") .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), + // TODO: should there be a grantedByRole or just a grantedByTrigger? + hasEntry("grantedByRoleIdName", "test_customer#xxx.owner"), hasEntry("grantedRoleIdName", "test_customer#xxx.admin"), hasEntry("granteeUserName", "customer-admin@xxx.example.com") ) )) .body("", hasItem( allOf( - hasEntry("grantedByRoleIdName", "global#global.admin"), + // TODO: should there be a grantedByRole or just a grantedByTrigger? + hasEntry("grantedByRoleIdName", "test_customer#yyy.owner"), hasEntry("grantedRoleIdName", "test_customer#yyy.admin"), hasEntry("granteeUserName", "customer-admin@yyy.example.com") ) @@ -296,7 +298,7 @@ class RbacGrantControllerAcceptanceTest extends ContextBasedTest { result.assertThat() .statusCode(403) .body("message", containsString("Access to granted role")) - .body("message", containsString("forbidden for {test_package#xxx00.admin}")); + .body("message", containsString("forbidden for test_package#xxx00.admin")); assertThat(findAllGrantsOf(givenCurrentUserAsPackageAdmin)) .extracting(RbacGrantEntity::getGranteeUserName) .doesNotContain(givenNewUser.getName()); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java index 3b09e861..8ce615b7 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantRepositoryIntegrationTest.java @@ -84,7 +84,7 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then exactlyTheseRbacGrantsAreReturned( result, - "{ grant role test_customer#xxx.admin to user customer-admin@xxx.example.com by role global#global.admin and assume }", + "{ grant role test_customer#xxx.admin to user customer-admin@xxx.example.com by role test_customer#xxx.owner and assume }", "{ grant role test_package#xxx00.admin to user pac-admin-xxx00@xxx.example.com by role test_customer#xxx.admin and assume }", "{ grant role test_package#xxx01.admin to user pac-admin-xxx01@xxx.example.com by role test_customer#xxx.admin and assume }", "{ grant role test_package#xxx02.admin to user pac-admin-xxx02@xxx.example.com by role test_customer#xxx.admin and assume }"); @@ -162,8 +162,8 @@ class RbacGrantRepositoryIntegrationTest extends ContextBasedTest { // then attempt.assertExceptionWithRootCauseMessage( JpaSystemException.class, - "ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid - + " forbidden for {test_package#xxx00.admin}"); + "ERROR: [403] Access to granted role test_package#xxx00.owner", + "forbidden for test_package#xxx00.admin"); jpaAttempt.transacted(() -> { // finally, we use the new user to make sure, no roles were granted context(given.arbitraryUser.getName(), null); diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java new file mode 100644 index 00000000..0e0421c8 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacgrant/RbacGrantsDiagramServiceIntegrationTest.java @@ -0,0 +1,103 @@ +package net.hostsharing.hsadminng.rbac.rbacgrant; + +import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.hs.office.test.ContextBasedTestWithCleanup; +import net.hostsharing.hsadminng.rbac.rbacgrant.RbacGrantsDiagramService.Include; +import net.hostsharing.test.JpaAttempt; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.EnumSet; +import java.util.UUID; + +import static java.lang.String.join; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import( { Context.class, JpaAttempt.class, RbacGrantsDiagramService.class}) +class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanup { + + @Autowired + RbacGrantsDiagramService grantsMermaidService; + + @MockBean + HttpServletRequest request; + + @Autowired + Context context; + + @Autowired + RbacGrantsDiagramService diagramService; + + TestInfo test; + + @BeforeEach + void init(TestInfo testInfo) { + this.test = testInfo; + } + + protected void context(final String currentUser, final String assumedRoles) { + context.define(test.getDisplayName(), null, currentUser, assumedRoles); + } + + protected void context(final String currentUser) { + context(currentUser, null); + } + + @Test + void allGrantsToCurrentUser() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant + role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_domain#xxx00-aaaa.owner --> role:test_package#xxx00.tenant + role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant + """.trim()); + } + + @Test + void allGrantsToCurrentUserIncludingPermissions() { + context("superuser-alex@hostsharing.net", "test_domain#xxx00-aaaa.owner"); + final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.TEST_ENTITIES, Include.PERMISSIONS)); + + assertThat(graph).isEqualTo(""" + flowchart TB + + role:test_customer#xxx.tenant --> perm:SELECT:on:test_customer#xxx + role:test_domain#xxx00-aaaa.admin --> perm:SELECT:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.admin --> role:test_package#xxx00.tenant + role:test_domain#xxx00-aaaa.owner --> perm:DELETE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.owner --> perm:UPDATE:on:test_domain#xxx00-aaaa + role:test_domain#xxx00-aaaa.owner --> role:test_domain#xxx00-aaaa.admin + role:test_domain#xxx00-aaaa.owner --> role:test_package#xxx00.tenant + role:test_package#xxx00.tenant --> perm:SELECT:on:test_package#xxx00 + role:test_package#xxx00.tenant --> role:test_customer#xxx.tenant + """.trim()); + } + + @Test + @Disabled // enable to generate from a real database + void print() throws IOException { + //context("superuser-alex@hostsharing.net", "hs_office_person#FirbySusan.admin"); + context("superuser-alex@hostsharing.net"); + + //final var graph = grantsMermaidService.allGrantsToCurrentUser(EnumSet.of(Include.NON_TEST_ENTITIES, Include.PERMISSIONS)); + + final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office_coopassetstransaction WHERE reference='ref 1000101-1'").getSingleResult(); + final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS)); + + RbacGrantsDiagramService.writeToFile(join(";", context.getAssumedRoles()), graph, "doc/all-grants.md"); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java new file mode 100644 index 00000000..d4256e56 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectEntity.java @@ -0,0 +1,31 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import lombok.*; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.annotation.Immutable; + +import jakarta.persistence.*; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "rbacobject") // TODO: create view rbacobject_ev +@Getter +@Setter +@ToString +@Immutable +@NoArgsConstructor +@AllArgsConstructor +public class RawRbacObjectEntity { + + @Id + private UUID uuid; + + @Column(name="objecttable") + private String objectTable; + + @NotNull + public static List objectDisplaysOf(@NotNull final List roles) { + return roles.stream().map(e -> e.objectTable+ "#" + e.uuid).sorted().toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java new file mode 100644 index 00000000..ab645316 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RawRbacObjectRepository.java @@ -0,0 +1,11 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.UUID; + +public interface RawRbacObjectRepository extends Repository { + + List findAll(); +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java index b13bcb76..9d7e16ca 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserControllerAcceptanceTest.java @@ -288,19 +288,15 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -313,7 +309,7 @@ class RbacUserControllerAcceptanceTest { RestAssured .given() .header("current-user", "superuser-alex@hostsharing.net") - .header("assumed-roles", "test_package#yyy00.admin") + .header("assumed-roles", "test_customer#yyy.admin") .port(port) .when() .get("http://localhost/api/rbac/users/" + givenUser.getUuid() + "/permissions") @@ -323,19 +319,15 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } @@ -357,19 +349,15 @@ class RbacUserControllerAcceptanceTest { .body("", hasItem( allOf( hasEntry("roleName", "test_customer#yyy.tenant"), - hasEntry("op", "view")) - )) - .body("", hasItem( - allOf( - hasEntry("roleName", "test_package#yyy00.admin"), - hasEntry("op", "add-domain")) + hasEntry("op", "SELECT")) )) .body("", hasItem( allOf( hasEntry("roleName", "test_domain#yyy00-aaaa.owner"), - hasEntry("op", "*")) + hasEntry("op", "DELETE")) )) - .body("size()", is(7)); + // actual content tested in integration test, so this is enough for here: + .body("size()", greaterThanOrEqualTo(6)); // @formatter:on } diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java index ea0a3109..c63047ed 100644 --- a/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepositoryIntegrationTest.java @@ -20,6 +20,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.List; import java.util.UUID; +import static java.util.Comparator.comparing; import static net.hostsharing.test.JpaAttempt.attempt; import static org.assertj.core.api.Assertions.assertThat; @@ -181,50 +182,48 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { private static final String[] ALL_USER_PERMISSIONS = Array.of( // @formatter:off - "global#global.admin -> global#global: add-customer", + "test_customer#xxx.admin -> test_customer#xxx: SELECT", + "test_customer#xxx.owner -> test_customer#xxx: DELETE", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", + "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.tenant -> test_package#xxx01: SELECT", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.tenant -> test_package#xxx02: SELECT", - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.owner -> test_customer#xxx: *", - "test_customer#xxx.tenant -> test_customer#xxx: view", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.owner -> test_customer#yyy: DELETE", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.admin -> test_package#yyy01: INSERT:test_domain", + "test_package#yyy01.tenant -> test_package#yyy01: SELECT", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.admin -> test_package#yyy02: INSERT:test_domain", + "test_package#yyy02.tenant -> test_package#yyy02: SELECT", - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.owner -> test_customer#yyy: *", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.admin -> test_package#yyy01: add-domain", - "test_package#yyy01.tenant -> test_package#yyy01: view", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.admin -> test_package#yyy02: add-domain", - "test_package#yyy02.tenant -> test_package#yyy02: view", - - "test_customer#zzz.admin -> test_customer#zzz: add-package", - "test_customer#zzz.admin -> test_customer#zzz: view", - "test_customer#zzz.owner -> test_customer#zzz: *", - "test_customer#zzz.tenant -> test_customer#zzz: view", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.admin -> test_package#zzz00: add-domain", - "test_package#zzz00.tenant -> test_package#zzz00: view", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.admin -> test_package#zzz01: add-domain", - "test_package#zzz01.tenant -> test_package#zzz01: view", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.admin -> test_package#zzz02: add-domain", - "test_package#zzz02.tenant -> test_package#zzz02: view" - // @formatter:on + "test_customer#zzz.admin -> test_customer#zzz: SELECT", + "test_customer#zzz.owner -> test_customer#zzz: DELETE", + "test_customer#zzz.tenant -> test_customer#zzz: SELECT", + "test_customer#zzz.admin -> test_customer#zzz: INSERT:test_package", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.admin -> test_package#zzz00: INSERT:test_domain", + "test_package#zzz00.tenant -> test_package#zzz00: SELECT", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.admin -> test_package#zzz01: INSERT:test_domain", + "test_package#zzz01.tenant -> test_package#zzz01: SELECT", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.admin -> test_package#zzz02: INSERT:test_domain", + "test_package#zzz02.tenant -> test_package#zzz02: SELECT" + // @formatter:on ); @Test @@ -233,7 +232,9 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { context("superuser-alex@hostsharing.net"); // when - final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-alex@hostsharing.net")); + final var result = rbacUserRepository.findPermissionsOfUserByUuid(userUUID("superuser-fran@hostsharing.net")) + .stream().filter(p -> p.getObjectTable().contains("test_")) + .sorted(comparing(RbacUserPermission::toString)).toList(); // then allTheseRbacPermissionsAreReturned(result, ALL_USER_PERMISSIONS); @@ -251,32 +252,32 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.admin -> test_customer#xxx: add-package", - "test_customer#xxx.admin -> test_customer#xxx: view", - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.admin -> test_customer#xxx: INSERT:test_package", + "test_customer#xxx.admin -> test_customer#xxx: SELECT", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: DELETE", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.admin -> test_package#xxx01: add-domain", - "test_package#xxx01.tenant -> test_package#xxx01: view", - "test_domain#xxx01-aaaa.owner -> test_domain#xxx01-aaaa: *", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.admin -> test_package#xxx01: INSERT:test_domain", + "test_package#xxx01.tenant -> test_package#xxx01: SELECT", + "test_domain#xxx01-aaaa.owner -> test_domain#xxx01-aaaa: DELETE", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.admin -> test_package#xxx02: add-domain", - "test_package#xxx02.tenant -> test_package#xxx02: view", - "test_domain#xxx02-aaaa.owner -> test_domain#xxx02-aaaa: *" + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.admin -> test_package#xxx02: INSERT:test_domain", + "test_package#xxx02.tenant -> test_package#xxx02: SELECT", + "test_domain#xxx02-aaaa.owner -> test_domain#xxx02-aaaa: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view" + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT" // @formatter:on ); } @@ -311,26 +312,26 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view", - "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: *", - "test_domain#xxx00-aaab.owner -> test_domain#xxx00-aaab: *" + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT", + "test_domain#xxx00-aaaa.owner -> test_domain#xxx00-aaaa: DELETE", + "test_domain#xxx00-aaab.owner -> test_domain#xxx00-aaab: DELETE" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-aaab.owner -> test_domain#yyy00-aaab: *" + "test_customer#yyy.admin -> test_customer#yyy: INSERT:test_package", + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-aaab.owner -> test_domain#yyy00-aaab: DELETE" // @formatter:on ); } @@ -359,11 +360,10 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { allTheseRbacPermissionsAreReturned( result, // @formatter:off - "test_customer#xxx.tenant -> test_customer#xxx: view", + "test_customer#xxx.tenant -> test_customer#xxx: SELECT", // "test_customer#xxx.admin -> test_customer#xxx: view" - Not permissions through the customer admin! - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.admin -> test_package#xxx00: add-domain", - "test_package#xxx00.tenant -> test_package#xxx00: view" + "test_package#xxx00.admin -> test_package#xxx00: INSERT:test_domain", + "test_package#xxx00.tenant -> test_package#xxx00: SELECT" // @formatter:on ); noneOfTheseRbacPermissionsAreReturned( @@ -373,13 +373,13 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { "test_customer#xxx.admin -> test_customer#xxx: add-package", // no permissions on other customer's objects "test_customer#yyy.admin -> test_customer#yyy: add-package", - "test_customer#yyy.admin -> test_customer#yyy: view", - "test_customer#yyy.tenant -> test_customer#yyy: view", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.admin -> test_package#yyy00: add-domain", - "test_package#yyy00.tenant -> test_package#yyy00: view", - "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: *", - "test_domain#yyy00-xxxb.owner -> test_domain#yyy00-xxxb: *" + "test_customer#yyy.admin -> test_customer#yyy: SELECT", + "test_customer#yyy.tenant -> test_customer#yyy: SELECT", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.admin -> test_package#yyy00: INSERT:test_domain", + "test_package#yyy00.tenant -> test_package#yyy00: SELECT", + "test_domain#yyy00-aaaa.owner -> test_domain#yyy00-aaaa: DELETE", + "test_domain#yyy00-xxxb.owner -> test_domain#yyy00-xxxb: DELETE" // @formatter:on ); } @@ -432,7 +432,8 @@ class RbacUserRepositoryIntegrationTest extends ContextBasedTest { final List actualResult, final String... expectedRoleNames) { assertThat(actualResult) - .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp()) + .extracting(p -> p.getRoleName() + " -> " + p.getObjectTable() + "#" + p.getObjectIdName() + ": " + p.getOp() + + (p.getOpTableName() != null ? (":"+p.getOpTableName()) : "" )) .contains(expectedRoleNames); } diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java index 6c695caa..942351c0 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerControllerAcceptanceTest.java @@ -148,7 +148,7 @@ class TestCustomerControllerAcceptanceTest { // finally, the new customer can be viewed by its own admin final var newUserUuid = UUID.fromString( location.substring(location.lastIndexOf('/') + 1)); - context.define("customer-admin@uuu.example.com"); + context.define("superuser-fran@hostsharing.net", "test_customer#uuu.admin"); assertThat(testCustomerRepository.findByUuid(newUserUuid)) .hasValueSatisfying(c -> assertThat(c.getPrefix()).isEqualTo("uuu")); } @@ -175,7 +175,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for test_customer#xxx.admin")); + .body("message", containsString("insert into test_customer not allowed for current subjects {test_customer#xxx.admin}")); // @formatter:on // finally, the new customer was not created @@ -204,7 +204,7 @@ class TestCustomerControllerAcceptanceTest { .statusCode(403) .contentType(ContentType.JSON) .statusCode(403) - .body("message", containsString("add-customer not permitted for customer-admin@yyy.example.com")); + .body("message", containsString("insert into test_customer not allowed for current subjects {customer-admin@yyy.example.com}")); // @formatter:on // finally, the new customer was not created diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java new file mode 100644 index 00000000..eca0aec1 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntityUnitTest.java @@ -0,0 +1,52 @@ +package net.hostsharing.hsadminng.test.cust; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestCustomerEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestCustomerEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{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: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:customer:owner ==> perm:customer:DELETE + role:customer:admin ==> perm:customer:UPDATE + role:customer:tenant ==> perm:customer:SELECT + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index ca535142..27458b14 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -10,8 +10,6 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Import; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; import java.util.List; @@ -27,9 +25,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestCustomerRepository testCustomerRepository; - @PersistenceContext - EntityManager em; - @MockBean HttpServletRequest request; @@ -43,7 +38,6 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { final var count = testCustomerRepository.count(); // when - final var result = attempt(em, () -> { final var newCustomer = new TestCustomerEntity( UUID.randomUUID(), "www", 90001, "customer-admin@www.example.com"); @@ -72,7 +66,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for test_customer#xxx.admin"); + "ERROR: [403] insert into test_customer not allowed for current subjects {test_customer#xxx.admin}"); } @Test @@ -90,7 +84,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { // then result.assertExceptionWithRootCauseMessage( PersistenceException.class, - "add-customer not permitted for customer-admin@xxx.example.com"); + "ERROR: [403] insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}"); } @@ -116,15 +110,15 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole_canViewAllCustomers() { + public void globalAdmin_withAssumedCustomerOwnerRole_canViewExactlyThatCustomer() { given: - context("superuser-alex@hostsharing.net", "global#global.admin"); + context("superuser-alex@hostsharing.net", "test_customer#yyy.owner"); // when final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); then: - allTheseCustomersAreReturned(result, "xxx", "yyy", "zzz"); + allTheseCustomersAreReturned(result, "yyy"); } @Test @@ -141,6 +135,8 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest { @Test public void customerAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnCustomer() { + context("customer-admin@xxx.example.com"); + context("customer-admin@xxx.example.com", "test_package#xxx00.admin"); final var result = testCustomerRepository.findCustomerByOptionalPrefixLike(null); diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java new file mode 100644 index 00000000..c5dccfd3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageEntityUnitTest.java @@ -0,0 +1,68 @@ +package net.hostsharing.hsadminng.test.pac; + +import net.hostsharing.hsadminng.rbac.rbacdef.RbacViewMermaidFlowchartGenerator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class TestPackageEntityUnitTest { + + @Test + void definesRbac() { + final var rbacFlowchart = new RbacViewMermaidFlowchartGenerator(TestPackageEntity.rbac()).toString(); + assertThat(rbacFlowchart).isEqualTo(""" + %%{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 + """); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java index 53d28e0c..a201d79e 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/pac/TestPackageRepositoryIntegrationTest.java @@ -1,6 +1,7 @@ package net.hostsharing.hsadminng.test.pac; import net.hostsharing.hsadminng.context.Context; +import net.hostsharing.hsadminng.context.ContextBasedTest; import net.hostsharing.test.JpaAttempt; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -19,10 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @Import( { Context.class, JpaAttempt.class }) -class TestPackageRepositoryIntegrationTest { - - @Autowired - Context context; +class TestPackageRepositoryIntegrationTest extends ContextBasedTest { @Autowired TestPackageRepository testPackageRepository; @@ -40,9 +38,10 @@ class TestPackageRepositoryIntegrationTest { class FindAllByOptionalNameLike { @Test - public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { // given - context.define("superuser-alex@hostsharing.net"); + // alex is not just global-admin but lso the creating user, thus we use fran + context.define("superuser-fran@hostsharing.net"); // when final var result = testPackageRepository.findAllByOptionalNameLike(null); @@ -52,7 +51,7 @@ class TestPackageRepositoryIntegrationTest { } @Test - public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() { + public void globalAdmin_withAssumedglobalAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotAssumed() { given: context.define("superuser-alex@hostsharing.net", "global#global.admin"); @@ -89,7 +88,7 @@ class TestPackageRepositoryIntegrationTest { class OptimisticLocking { @Test - public void supportsOptimisticLocking() throws InterruptedException { + public void supportsOptimisticLocking() { // given globalAdminWithAssumedRole("test_package#xxx00.admin"); final var pac = testPackageRepository.findAllByOptionalNameLike("%").get(0);