RBAC Diagram+PostgreSQL Generator and view->SELECT etc. refactoring (#21)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #21
Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
Michael Hoennig 2024-03-11 12:30:43 +01:00
parent d9558f2cfe
commit 187c0db8e2
91 changed files with 4181 additions and 856 deletions

View File

@ -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, <u>what</u> 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

View File

@ -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;

View File

@ -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'));

View File

@ -1,6 +1,6 @@
package net.hostsharing.hsadminng.errors;
import net.hostsharing.hsadminng.persistence.HasUuid;
import net.hostsharing.hsadminng.rbac.rbacobject.RbacObject;
import java.util.UUID;
@ -8,7 +8,7 @@ public class ReferenceNotFoundException extends RuntimeException {
private final Class<?> entityClass;
private final UUID uuid;
public <E extends HasUuid> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
public <E extends RbacObject> ReferenceNotFoundException(final Class<E> entityClass, final UUID uuid, final Throwable exc) {
super(exc);
this.entityClass = entityClass;
this.uuid = uuid;

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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 extends HasUuid> E ref(final Class<E> entityClass, final UUID uuid) {
private <E extends RbacObject> E ref(final Class<E> entityClass, final UUID uuid) {
try {
return em.getReference(entityClass, uuid);
} catch (final Throwable exc) {

View File

@ -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() : "<empty details>";
: dateOfDeath != null ? dateOfDeath.toString()
: "<empty details>";
}
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");
}
}

View File

@ -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("<person=null>");
}
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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -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 {
}

View File

@ -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<RbacView.RbacGrantDefinition> getInsertGrants() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT);
}
private Optional<RbacView.RbacGrantDefinition> getOptionalInsertGrant() {
return getInsertGrants()
.reduce(singleton());
}
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
return getInsertGrants()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.reduce(singleton());
}
private static <T> BinaryOperator<T> singleton() {
return (x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");
};
}
private static String toVar(final RbacView.RbacRoleDefinition roleDef) {
return uncapitalize(roleDef.getEntityAlias().simpleName()) + capitalize(roleDef.getRole().roleName());
}
}

View File

@ -0,0 +1,5 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
public enum PostgresTriggerReference {
NEW, OLD
}

View File

@ -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("--//");
}
}

View File

@ -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("""
-- ========<