diff --git a/doc/rbac.md b/doc/rbac.md index e8799d5f..9aa4b024 100644 --- a/doc/rbac.md +++ b/doc/rbac.md @@ -171,10 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject* An *RbacOperation* determines, what an *RbacPermission* allows to do. It can be one of: -- **'INSERT'** - permits inserting new rows related to the row, to which the permission belongs, in the table which is specified an extra column -- **'SELECT'** - permits selecting the row specified by the permission -- **'UPDATE'** - permits updating (only the updatable columns of) the row specified by the permission -- **'DELETE'** - permits deleting the row 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. @@ -620,10 +620,10 @@ Let's have a look at the two view queries: WHERE target.uuid IN ( SELECT uuid FROM queryAccessibleObjectUuidsOfSubjectIds( - 'SELECTÄ, 'customer', currentSubjectsUuids())); + 'SELECT, 'customer', currentSubjectsUuids())); This view should be automatically updatable. -Where, for updates, we actually have to check for 'UPDATE' instead of 'SELECTÄ 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. @@ -638,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( - 'SELECTÄ, 'customer', currentSubjectsUuids()) AS allowedObjId + 'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId ON target.uuid = allowedObjId; This view cannot is not updatable automatically, diff --git a/sql/rbac-view-option-experiments.sql b/sql/rbac-view-option-experiments.sql index 2c4508ae..f6e80e10 100644 --- a/sql/rbac-view-option-experiments.sql +++ b/sql/rbac-view-option-experiments.sql @@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer TO restricted USING ( -- id=1000 - isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), 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(findPermissionId('test_customer', id, 'SELECT'), 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 = 'SELECT'; + 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 = 'SELECT'; + 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 = 'SELECT'); + where objectTable='test_customer'); call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com')); diff --git a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java index 8101286b..d419806e 100644 --- a/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/test/cust/TestCustomerEntity.java @@ -14,7 +14,6 @@ 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; @@ -43,7 +42,7 @@ public class TestCustomerEntity implements HasUuid { .withUpdatableColumns("reference", "prefix", "adminUserName") .createRole(OWNER, (with) -> { - // with.owningUser(CREATOR); TODO: needs assumed role + // with.owningUser(CREATOR); FIXME: needs assumed role, was: getRbacUserId(NEW.adminUserName, 'create') with.incomingSuperRole(GLOBAL, ADMIN); with.permission(DELETE); }) diff --git a/src/main/resources/db/changelog/010-context.sql b/src/main/resources/db/changelog/010-context.sql index 11d52f50..521e4812 100644 --- a/src/main/resources/db/changelog/010-context.sql +++ b/src/main/resources/db/changelog/010-context.sql @@ -66,11 +66,11 @@ begin when others then currentTask := null; end; --- TODO: uncomment +-- FIXME: uncomment -- if (currentTask is null or currentTask = '') then -- raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; -- end if; - return 'unknown'; -- TODO: currentTask; + return 'unknown'; -- FIXME: currentTask; end; $$; --// diff --git a/src/main/resources/db/changelog/050-rbac-base.sql b/src/main/resources/db/changelog/050-rbac-base.sql index 2eeff958..c477cf3b 100644 --- a/src/main/resources/db/changelog/050-rbac-base.sql +++ b/src/main/resources/db/changelog/050-rbac-base.sql @@ -366,6 +366,7 @@ create trigger deleteRbacRolesOfRbacObject_Trigger */ create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone +-- FIXME: uncomment check -- check ( -- VALUE = 'INSERT' or -- VALUE = 'DELETE' or @@ -389,17 +390,6 @@ create table RbacPermission call create_journal('RbacPermission'); -create or replace function permissionExists(forObjectUuid uuid, forOp RbacOp) - returns bool - language sql as $$ -select exists( - select op - from RbacPermission p - where p.objectUuid = forObjectUuid - and p.op = forOp - ); -$$; - create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) returns uuid language plpgsql as $$ @@ -462,7 +452,7 @@ begin end; $$; -create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null ) +create or replace function findPermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) returns uuid returns null on null input stable -- leakproof @@ -474,6 +464,17 @@ select uuid and p.opTableName = forOpTableName $$; +create or replace function findEffectivePermissionId(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null) + returns uuid + returns null on null input + stable -- leakproof + language sql as $$ +select uuid + from RbacPermission p + where p.objectUuid = forObjectUuid + and (forOp = 'SELECT' or p.op = forOp) -- all other RbacOp include 'SELECT' + and p.opTableName = forOpTableName +$$; --// -- ============================================================================ @@ -748,7 +749,7 @@ begin select descendantUuid from grants) as granted join RbacPermission perm - on granted.descendantUuid = perm.uuid and perm.op = requiredOp + on granted.descendantUuid = perm.uuid and (requiredOp = 'SELECT' or perm.op = requiredOp) join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable limit maxObjects + 1; diff --git a/src/main/resources/db/changelog/051-rbac-user-grant.sql b/src/main/resources/db/changelog/051-rbac-user-grant.sql index 05332ed9..b71869f9 100644 --- a/src/main/resources/db/changelog/051-rbac-user-grant.sql +++ b/src/main/resources/db/changelog/051-rbac-user-grant.sql @@ -37,17 +37,28 @@ end; $$; create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true) language plpgsql as $$ +declare + grantedByRoleIdName text; + grantedRoleIdName text; begin perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole'); perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole'); perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser'); - if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then - raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects(); - end if; + assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null'; + assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null'; + assert userUuid is not null, 'userUuid must not be null'; + if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then + select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName; + raise exception '[403] Access to granted-by-role % (%) forbidden for % (%)', + grantedByRoleIdName, grantedByRoleUuid, currentSubjects(), currentSubjectsUuids(); + end if; if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then - raise exception '[403] Access to granted role % forbidden for %', grantedRoleUuid, currentSubjects(); + select roleIdName from rbacRole_ev where uuid=grantedByRoleUuid into grantedByRoleIdName; + select roleIdName from rbacRole_ev where uuid=grantedRoleUuid into grantedRoleIdName; + raise exception '[403] Access to granted role % (%) forbidden for % (%)', + grantedRoleIdName, grantedRoleUuid, grantedByRoleUuid, grantedByRoleIdName; end if; insert diff --git a/src/main/resources/db/changelog/118-test-customer-test-data.sql b/src/main/resources/db/changelog/118-test-customer-test-data.sql index 643022fe..1e239001 100644 --- a/src/main/resources/db/changelog/118-test-customer-test-data.sql +++ b/src/main/resources/db/changelog/118-test-customer-test-data.sql @@ -46,8 +46,8 @@ begin select * into newCust from test_customer where reference=custReference; call grantRoleToUser( + getRoleId(testCustomerOwner(newCust), 'fail'), getRoleId(testCustomerAdmin(newCust), 'fail'), - findRoleId(testCustomerOwner(newCust)), custAdminUuid, true); end; $$; diff --git a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java index 55c958d5..b21f2d5d 100644 --- a/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/test/cust/TestCustomerRepositoryIntegrationTest.java @@ -16,7 +16,6 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceException; import jakarta.servlet.http.HttpServletRequest; -import java.util.EnumSet; import java.util.List; import java.util.UUID; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a4f570f9..01e283b9 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -4,8 +4,8 @@ spring: platform: postgres datasource: - url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers - url-local: jdbc:postgresql://localhost:5432/postgres + url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers + url: jdbc:postgresql://localhost:5432/postgres username: postgres password: password