Compare commits

..

2 Commits

Author SHA1 Message Date
Michael Hoennig
b37e8044b2 implement insert trigger if no explicit grant rule is specified 2024-03-07 12:26:07 +01:00
Michael Hoennig
20de9ba7a4 fixes and improvements after self-review 2024-03-07 11:27:21 +01:00
14 changed files with 128 additions and 60 deletions

View File

@ -171,10 +171,10 @@ An *RbacPermission* allows a specific *RbacOperation* on a specific *RbacObject*
An *RbacOperation* determines, <u>what</u> an *RbacPermission* allows to do. An *RbacOperation* determines, <u>what</u> an *RbacPermission* allows to do.
It can be one of: 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 - **'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 - **'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 - **'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 - **'DELETE'** - permits deleting the row specified by the permission, includes 'SELECT'
This list is extensible according to the needs of the access rule system. 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 ( WHERE target.uuid IN (
SELECT uuid SELECT uuid
FROM queryAccessibleObjectUuidsOfSubjectIds( FROM queryAccessibleObjectUuidsOfSubjectIds(
'SELECTÄ, 'customer', currentSubjectsUuids())); 'SELECT, 'customer', currentSubjectsUuids()));
This view should be automatically updatable. 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. With the larger dataset, the test suite initially needed over 7 seconds with this view query.
At this point the second variant was tried. 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.* SELECT DISTINCT target.*
FROM customer AS target FROM customer AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( JOIN queryAccessibleObjectUuidsOfSubjectIds(
'SELECTÄ, 'customer', currentSubjectsUuids()) AS allowedObjId 'SELECT, 'customer', currentSubjectsUuids()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
This view cannot is not updatable automatically, This view cannot is not updatable automatically,

View File

@ -20,7 +20,7 @@ CREATE POLICY customer_policy ON customer
TO restricted TO restricted
USING ( USING (
-- id=1000 -- id=1000
isPermissionGrantedToSubject(findPermissionId('test_customer', id, 'SELECT'), currentUserUuid()) isPermissionGrantedToSubject(findEffectivePermissionId('test_customer', id, 'SELECT'), currentUserUuid())
); );
SET SESSION AUTHORIZATION restricted; SET SESSION AUTHORIZATION restricted;
@ -35,7 +35,7 @@ SELECT * FROM customer;
CREATE OR REPLACE RULE "_RETURN" AS CREATE OR REPLACE RULE "_RETURN" AS
ON SELECT TO cust_view ON SELECT TO cust_view
DO INSTEAD 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 * from cust_view LIMIT 10;
select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net')); select queryAllPermissionsOfSubjectId(findRbacUser('superuser-alex@hostsharing.net'));
@ -52,7 +52,7 @@ CREATE OR REPLACE RULE "_RETURN" AS
DO INSTEAD DO INSTEAD
SELECT c.uuid, c.reference, c.prefix FROM customer AS c SELECT c.uuid, c.reference, c.prefix FROM customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p 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; GRANT ALL PRIVILEGES ON cust_view TO restricted;
SET SESSION SESSION AUTHORIZATION restricted; SET SESSION SESSION AUTHORIZATION restricted;
@ -68,7 +68,7 @@ CREATE OR REPLACE VIEW cust_view AS
SELECT c.uuid, c.reference, c.prefix SELECT c.uuid, c.reference, c.prefix
FROM customer AS c FROM customer AS c
JOIN queryAllPermissionsOfSubjectId(currentUserUuid()) AS p 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; GRANT ALL PRIVILEGES ON cust_view TO restricted;
SET SESSION SESSION AUTHORIZATION 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 join RbacReference RR on g.ascendantUuid = RR.uuid
where g.descendantUuid in ( where g.descendantUuid in (
select uuid from queryAllPermissionsOfSubjectId(findRbacUser('alex@example.com')) 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')); call grantRoleToUser(findRoleId('test_customer#aaa.admin'), findRbacUser('aaaaouq@example.com'));

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacdef; package net.hostsharing.hsadminng.rbac.rbacdef;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.INSERT; 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.RbacView.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
@ -95,10 +96,6 @@ public class InsertTriggerGenerator {
} }
private void generateInsertCheckTrigger(final StringWriter plPgSql) { private void generateInsertCheckTrigger(final StringWriter plPgSql) {
rbacDef.getGrantDefs().stream()
.filter(g -> g.isToCreate() && g.grantType() == PERM_TO_ROLE &&
g.getPermDef().getPermission() == INSERT )
.forEach(g -> {
plPgSql.writeLn(""" plPgSql.writeLn("""
/** /**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}. Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
@ -107,24 +104,51 @@ public class InsertTriggerGenerator {
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
raise exception 'insert into ${rawSubTable} not allowed for current subjects %', currentSubjectsUuids(); raise exception 'insert into ${rawSubTable} not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$; end; $$;
""",
with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()));
getOptionalInsertGrant().ifPresentOrElse(g -> {
plPgSql.writeLn("""
create trigger ${rawSubTable}_insert_permission_check_tg create trigger ${rawSubTable}_insert_permission_check_tg
before insert on ${rawSubTable} before insert on ${rawSubTable}
for each row for each row
when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') ) when ( not hasInsertPermission(NEW.${referenceColumn}, 'INSERT', '${rawSubTable}') )
execute procedure ${rawSubTable}_insert_permission_missing_tf(); execute procedure ${rawSubTable}_insert_permission_missing_tf();
""", """,
with("rawSubTable", g.getPermDef().entityAlias.getRawTableName()), with("rawSubTable", rbacDef.getRootEntityAlias().getRawTableName()),
with("referenceColumn", g.getSuperRoleDef().getEntityAlias().dependsOnColumName() )); 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((x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");
}); });
} }
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() { private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
return rbacDef.getGrantDefs().stream() return getInsertGrants()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT)
.map(RbacView.RbacGrantDefinition::getSuperRoleDef) .map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.reduce((x, y) -> { .reduce((x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed"); throw new IllegalStateException("only a single INSERT permission grant allowed");

View File

@ -334,6 +334,7 @@ class RolesGrantsAndPermissionsGenerator {
.map(RbacPermissionDefinition::getPermission) .map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::permission) .map(RbacView.Permission::permission)
.map(p -> "'" + p + "'") .map(p -> "'" + p + "'")
.sorted()
.toList(); .toList();
plPgSql.indented(() -> plPgSql.indented(() ->
plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n")); plPgSql.writeLn("permissions => array[" + joinArrayElements(arrayElements, 3) + "],\n"));

View File

@ -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.GLOBAL;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.Permission.*; 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.Role.*;
import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor; import static net.hostsharing.hsadminng.rbac.rbacdef.RbacView.rbacViewFor;
@ -43,7 +42,7 @@ public class TestCustomerEntity implements HasUuid {
.withUpdatableColumns("reference", "prefix", "adminUserName") .withUpdatableColumns("reference", "prefix", "adminUserName")
.createRole(OWNER, (with) -> { .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.incomingSuperRole(GLOBAL, ADMIN);
with.permission(DELETE); with.permission(DELETE);
}) })

View File

@ -66,11 +66,11 @@ begin
when others then when others then
currentTask := null; currentTask := null;
end; end;
-- TODO: uncomment -- FIXME: uncomment
-- if (currentTask is null or currentTask = '') then -- if (currentTask is null or currentTask = '') then
-- raise exception '[401] currentTask must be defined, please call `defineContext(...)`'; -- raise exception '[401] currentTask must be defined, please call `defineContext(...)`';
-- end if; -- end if;
return 'unknown'; -- TODO: currentTask; return 'unknown'; -- FIXME: currentTask;
end; $$; end; $$;
--// --//

View File

@ -366,6 +366,7 @@ create trigger deleteRbacRolesOfRbacObject_Trigger
*/ */
create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone create domain RbacOp as varchar(67) -- TODO: shorten to 8, once the deprecated values are gone
-- FIXME: uncomment check
-- check ( -- check (
-- VALUE = 'INSERT' or -- VALUE = 'INSERT' or
-- VALUE = 'DELETE' or -- VALUE = 'DELETE' or
@ -389,17 +390,6 @@ create table RbacPermission
call create_journal('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) create or replace function createPermission(forObjectUuid uuid, forOp RbacOp, forOpTableName text = null)
returns uuid returns uuid
language plpgsql as $$ language plpgsql as $$
@ -474,6 +464,17 @@ select uuid
and p.opTableName = forOpTableName 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 select descendantUuid
from grants) as granted from grants) as granted
join RbacPermission perm 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 join RbacObject obj on obj.uuid = perm.objectUuid and obj.objectTable = forObjectTable
limit maxObjects + 1; limit maxObjects + 1;

View File

@ -37,17 +37,28 @@ end; $$;
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true) create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
language plpgsql as $$ language plpgsql as $$
declare
grantedByRoleIdName text;
grantedRoleIdName text;
begin begin
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole'); perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole'); perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser'); perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
if NOT isGranted(currentSubjectsUuids(), grantedByRoleUuid) then assert grantedByRoleUuid is not null, 'grantedByRoleUuid must not be null';
raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects(); assert grantedRoleUuid is not null, 'grantedRoleUuid must not be null';
end if; 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 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; end if;
insert insert

View File

@ -22,6 +22,19 @@ grant select on global to ${HSADMINNG_POSTGRES_RESTRICTED_USERNAME};
--// --//
-- ============================================================================
--changeset rbac-global-IS-GLOBAL-ADMIN:1 endDelimiter:--//
-- ------------------------------------------------------------------
create or replace function isGlobalAdmin()
returns boolean
language plpgsql as $$
begin
return isGranted(currentSubjectsUuids(), findRoleId(globalAdmin()));
end; $$;
--//
-- ============================================================================ -- ============================================================================
--changeset rbac-global-HAS-GLOBAL-PERMISSION:1 endDelimiter:--// --changeset rbac-global-HAS-GLOBAL-PERMISSION:1 endDelimiter:--//
-- ------------------------------------------------------------------ -- ------------------------------------------------------------------

View File

@ -1,5 +1,5 @@
--liquibase formatted sql --liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.239729250. -- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.376742633.
-- ============================================================================ -- ============================================================================
@ -80,6 +80,25 @@ execute procedure insertTriggerForTestCustomer_tf();
--changeset test-customer-rbac-INSERT:1 endDelimiter:--// --changeset test-customer-rbac-INSERT:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
/**
Checks if the user or assumed roles are allowed to insert a row to test_customer.
*/
create or replace function test_customer_insert_permission_missing_tf()
returns trigger
language plpgsql as $$
begin
raise exception 'insert into test_customer not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$;
create trigger test_customer_insert_permission_check_tg
before insert on test_customer
for each row
-- As there is no explicit INSERT grant specified for this table,
-- only global admins are allowed to insert any rows.
when ( not isGlobalAdmin() )
execute procedure test_customer_insert_permission_missing_tf();
--// --//
-- ============================================================================ -- ============================================================================

View File

@ -46,8 +46,8 @@ begin
select * into newCust select * into newCust
from test_customer where reference=custReference; from test_customer where reference=custReference;
call grantRoleToUser( call grantRoleToUser(
getRoleId(testCustomerOwner(newCust), 'fail'),
getRoleId(testCustomerAdmin(newCust), 'fail'), getRoleId(testCustomerAdmin(newCust), 'fail'),
findRoleId(testCustomerOwner(newCust)),
custAdminUuid, custAdminUuid,
true); true);
end; $$; end; $$;

View File

@ -1,5 +1,5 @@
--liquibase formatted sql --liquibase formatted sql
-- This code generated was by RbacViewPostgresGenerator at 2024-03-06T15:40:13.277446553. -- This code generated was by RbacViewPostgresGenerator at 2024-03-07T12:25:36.422351715.
-- ============================================================================ -- ============================================================================
@ -194,7 +194,8 @@ create or replace function test_package_insert_permission_missing_tf()
returns trigger returns trigger
language plpgsql as $$ language plpgsql as $$
begin begin
raise exception 'insert into test_package not allowed for current subjects %', currentSubjectsUuids(); raise exception 'insert into test_package not allowed for current subjects % (%)',
currentSubjects(), currentSubjectsUuids();
end; $$; end; $$;
create trigger test_package_insert_permission_check_tg create trigger test_package_insert_permission_check_tg

View File

@ -16,7 +16,6 @@ import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContext;
import jakarta.persistence.PersistenceException; import jakarta.persistence.PersistenceException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import java.util.EnumSet;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -75,7 +74,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
PersistenceException.class, PersistenceException.class,
"add-customer not permitted for test_customer#xxx.admin"); "ERROR: insert into test_customer not allowed for current subjects {test_customer#xxx.admin}");
} }
@Test @Test
@ -93,7 +92,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
// then // then
result.assertExceptionWithRootCauseMessage( result.assertExceptionWithRootCauseMessage(
PersistenceException.class, PersistenceException.class,
"add-customer not permitted for customer-admin@xxx.example.com"); "ERROR: insert into test_customer not allowed for current subjects {customer-admin@xxx.example.com}");
} }

View File

@ -4,8 +4,8 @@ spring:
platform: postgres platform: postgres
datasource: datasource:
url: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers url-tc: jdbc:tc:postgresql:15.5-bookworm:///spring_boot_testcontainers
url-local: jdbc:postgresql://localhost:5432/postgres url: jdbc:postgresql://localhost:5432/postgres
username: postgres username: postgres
password: password password: password