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.
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,

View File

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

View File

@ -1,6 +1,7 @@
package net.hostsharing.hsadminng.rbac.rbacdef;
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.RbacGrantDefinition.GrantType.PERM_TO_ROLE;
@ -95,10 +96,6 @@ public class InsertTriggerGenerator {
}
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("""
/**
Checks if the user or assumed roles are allowed to insert a row to ${rawSubTable}.
@ -107,24 +104,51 @@ public class InsertTriggerGenerator {
returns trigger
language plpgsql as $$
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; $$;
""",
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", g.getPermDef().entityAlias.getRawTableName()),
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((x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");
});
}
private Optional<RbacView.RbacRoleDefinition> getOptionalInsertSuperRole() {
return rbacDef.getGrantDefs().stream()
.filter(g -> g.grantType() == PERM_TO_ROLE)
.filter(g -> g.getPermDef().toCreate && g.getPermDef().getPermission() == INSERT)
return getInsertGrants()
.map(RbacView.RbacGrantDefinition::getSuperRoleDef)
.reduce((x, y) -> {
throw new IllegalStateException("only a single INSERT permission grant allowed");

View File

@ -334,6 +334,7 @@ class RolesGrantsAndPermissionsGenerator {
.map(RbacPermissionDefinition::getPermission)
.map(RbacView.Permission::permission)
.map(p -> "'" + p + "'")
.sorted()
.toList();
plPgSql.indented(() ->
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.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);
})

View File

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

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

View File

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

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:--//
-- ------------------------------------------------------------------

View File

@ -1,5 +1,5 @@
--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:--//
-- ----------------------------------------------------------------------------
/**
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
from test_customer where reference=custReference;
call grantRoleToUser(
getRoleId(testCustomerOwner(newCust), 'fail'),
getRoleId(testCustomerAdmin(newCust), 'fail'),
findRoleId(testCustomerOwner(newCust)),
custAdminUuid,
true);
end; $$;

View File

@ -1,5 +1,5 @@
--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
language plpgsql as $$
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; $$;
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.PersistenceException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.EnumSet;
import java.util.List;
import java.util.UUID;
@ -75,7 +74,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
// then
result.assertExceptionWithRootCauseMessage(
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
@ -93,7 +92,7 @@ class TestCustomerRepositoryIntegrationTest extends ContextBasedTest {
// then
result.assertExceptionWithRootCauseMessage(
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
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