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:
parent
d9558f2cfe
commit
187c0db8e2
148
doc/rbac.md
148
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, <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
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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'));
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package net.hostsharing.hsadminng.rbac.rbacdef;
|
||||
|
||||
public enum PostgresTriggerReference {
|
||||
NEW, OLD
|
||||
}
|
@ -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("--//");
|
||||
}
|
||||
}
|
@ -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("""
|
||||
-- ========< |