Compare commits
2 Commits
4e2b17a216
...
b37e8044b2
Author | SHA1 | Date | |
---|---|---|---|
|
b37e8044b2 | ||
|
20de9ba7a4 |
14
doc/rbac.md
14
doc/rbac.md
@ -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,
|
||||
|
@ -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'));
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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"));
|
||||
|
@ -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);
|
||||
})
|
||||
|
@ -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; $$;
|
||||
--//
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:--//
|
||||
-- ------------------------------------------------------------------
|
||||
|
@ -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();
|
||||
|
||||
--//
|
||||
|
||||
-- ============================================================================
|
||||
|
@ -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; $$;
|
||||
|
@ -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
|
||||
|
@ -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}");
|
||||
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user