RbacGrants with follow=false for customer.owner to customer.admin

This commit is contained in:
Michael Hoennig 2022-07-22 16:52:49 +02:00
parent f2d0fbe67a
commit 377b63ca3d
7 changed files with 170 additions and 64 deletions

View File

@ -39,7 +39,7 @@ CREATE TABLE RbacGrants
( (
ascendantUuid uuid references RbacReference (uuid) ON DELETE CASCADE, ascendantUuid uuid references RbacReference (uuid) ON DELETE CASCADE,
descendantUuid uuid references RbacReference (uuid) ON DELETE CASCADE, descendantUuid uuid references RbacReference (uuid) ON DELETE CASCADE,
-- apply bool not null, -- alternative 1 to implement assumable roles follow boolean not null default true,
primary key (ascendantUuid, descendantUuid) primary key (ascendantUuid, descendantUuid)
); );
CREATE INDEX ON RbacGrants (ascendantUuid); CREATE INDEX ON RbacGrants (ascendantUuid);
@ -254,23 +254,19 @@ BEGIN
END; END;
$$; $$;
CREATE OR REPLACE PROCEDURE grantRoleToRole(subRoleId uuid, superRoleId uuid CREATE OR REPLACE PROCEDURE grantRoleToRole(subRoleId uuid, superRoleId uuid, doFollow bool = true )
-- , doapply bool = true -- assumeV1
)
LANGUAGE plpgsql AS $$ LANGUAGE plpgsql AS $$
BEGIN BEGIN
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole'); perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole'); perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
RAISE NOTICE 'granting subRole % to superRole %', subRoleId, superRoleId; -- TODO: remove
IF ( isGranted(subRoleId, superRoleId) ) THEN IF ( isGranted(subRoleId, superRoleId) ) THEN
RAISE EXCEPTION 'Cyclic role grant detected between % and %', subRoleId, superRoleId; RAISE EXCEPTION 'Cyclic role grant detected between % and %', subRoleId, superRoleId;
END IF; END IF;
-- INSERT INTO RbacGrants (ascendantUuid, descendantUuid, apply) VALUES (superRoleId, subRoleId, doapply); -- assumeV1 -- INSERT INTO RbacGrants (ascendantUuid, descendantUuid, apply) VALUES (superRoleId, subRoleId, doapply); -- assumeV1
INSERT INTO RbacGrants (ascendantUuid, descendantUuid) VALUES (superRoleId, subRoleId) INSERT INTO RbacGrants (ascendantUuid, descendantUuid, follow)VALUES (superRoleId, subRoleId, doFollow)
ON CONFLICT DO NOTHING ; -- TODO: remove ON CONFLICT DO NOTHING ; -- TODO: remove?
END; $$; END; $$;
CREATE OR REPLACE PROCEDURE revokeRoleFromRole(subRoleId uuid, superRoleId uuid) CREATE OR REPLACE PROCEDURE revokeRoleFromRole(subRoleId uuid, superRoleId uuid)
@ -298,40 +294,52 @@ END; $$;
abort; abort;
set local session authorization default; set local session authorization default;
CREATE OR REPLACE FUNCTION nextLevel(level integer, maxDepth integer)
RETURNS INTEGER
LANGUAGE plpgsql AS $$
BEGIN
IF (level > maxDepth) THEN
RAISE WARNING 'Role assignment depth exceeded %/%.', level, maxDepth;
END IF;
RETURN level+1;
END;
$$;
CREATE OR REPLACE FUNCTION queryAccessibleObjectUuidsOfSubjectIds( CREATE OR REPLACE FUNCTION queryAccessibleObjectUuidsOfSubjectIds(
requiredOp RbacOp, requiredOp RbacOp,
-- objectTable varchar, -- TODO: maybe another optimization? but test perforamance for joins! forObjectTable varchar, -- TODO: test perforamance in joins!
subjectIds uuid[], subjectIds uuid[],
maxDepth integer = 8,
maxObjects integer = 16000) maxObjects integer = 16000)
RETURNS SETOF uuid RETURNS SETOF uuid
RETURNS NULL ON NULL INPUT RETURNS NULL ON NULL INPUT
LANGUAGE plpgsql AS $$ LANGUAGE plpgsql AS $$
DECLARE DECLARE
foundRows bigint; foundRows bigint;
BEGIN BEGIN
RETURN QUERY SELECT DISTINCT perm.objectUuid RETURN QUERY SELECT DISTINCT perm.objectUuid
FROM ( FROM (
WITH RECURSIVE grants AS ( WITH RECURSIVE grants AS (
SELECT descendantUuid, ascendantUuid, 1 AS level SELECT descendantUuid, ascendantUuid, 1 AS level
FROM RbacGrants FROM RbacGrants
WHERE ascendantUuid = ANY(subjectIds) WHERE follow AND ascendantUuid = ANY(subjectIds)
UNION ALL UNION DISTINCT
SELECT "grant".descendantUuid, "grant".ascendantUuid, level + 1 AS level SELECT "grant".descendantUuid, "grant".ascendantUuid, level+1 AS level
FROM RbacGrants "grant" FROM RbacGrants "grant"
INNER JOIN grants recur ON recur.descendantUuid = "grant".ascendantUuid INNER JOIN grants recur ON recur.descendantUuid = "grant".ascendantUuid
WHERE level <= maxDepth WHERE follow
) SELECT descendantUuid ) SELECT descendantUuid
FROM grants FROM grants
-- LIMIT maxObjects+1
) as granted ) as granted
JOIN RbacPermission perm ON granted.descendantUuid=perm.uuid AND perm.op IN ('*', requiredOp); JOIN RbacPermission perm
ON granted.descendantUuid=perm.uuid AND perm.op IN ('*', requiredOp)
JOIN RbacObject obj ON obj.uuid=perm.objectUuid AND obj.objectTable=forObjectTable;
foundRows = lastRowCount(); foundRows = lastRowCount();
IF foundRows > maxObjects THEN IF foundRows > maxObjects THEN
RAISE EXCEPTION 'Too many accessible objects, limit is %, found %.', maxObjects, foundRows RAISE EXCEPTION 'Too many accessible objects, limit is %, found %.', maxObjects, foundRows
USING USING
ERRCODE = 'P0003', -- 'HS-ADMIN-NG:ACC-OBJ-EXC', ERRCODE = 'P0003',
HINT = 'Please assume a sub-role and try again.'; HINT = 'Please assume a sub-role and try again.';
END IF; END IF;
END; END;
@ -340,9 +348,9 @@ $$;
abort; abort;
set local session authorization restricted; set local session authorization restricted;
begin transaction; begin transaction;
set local statement_timeout TO '60s'; set local statement_timeout TO '5s';
select count(*) select count(*)
from queryAccessibleObjectUuidsOfSubjectIds('view', ARRAY[findRbacUser('mike@hostsharing.net')], 4, 10000); from queryAccessibleObjectUuidsOfSubjectIds('view', 'customer', ARRAY[findRbacUser('mike@hostsharing.net')], 10000);
end transaction; end transaction;
--- ---
@ -510,27 +518,20 @@ CREATE OR REPLACE FUNCTION isGranted(granteeId uuid, grantedId uuid)
RETURNS bool RETURNS bool
RETURNS NULL ON NULL INPUT RETURNS NULL ON NULL INPUT
LANGUAGE sql AS $$ LANGUAGE sql AS $$
SELECT granteeId=grantedId OR granteeId IN ( SELECT granteeId=grantedId OR granteeId IN (
WITH RECURSIVE grants AS ( WITH RECURSIVE grants AS (
SELECT SELECT descendantUuid, ascendantUuid
descendantUuid, FROM RbacGrants
ascendantUuid WHERE descendantUuid = grantedId
FROM UNION ALL
RbacGrants SELECT "grant".descendantUuid, "grant".ascendantUuid
WHERE FROM RbacGrants "grant"
descendantUuid = grantedId
UNION ALL
SELECT
"grant".descendantUuid,
"grant".ascendantUuid
FROM
RbacGrants "grant"
INNER JOIN grants recur ON recur.ascendantUuid = "grant".descendantUuid INNER JOIN grants recur ON recur.ascendantUuid = "grant".descendantUuid
) SELECT ) SELECT
ascendantUuid ascendantUuid
FROM FROM
grants grants
); );
$$; $$;
CREATE OR REPLACE FUNCTION isPermissionGrantedToSubject(permissionId uuid, subjectId uuid) CREATE OR REPLACE FUNCTION isPermissionGrantedToSubject(permissionId uuid, subjectId uuid)
@ -617,17 +618,17 @@ DECLARE
BEGIN BEGIN
BEGIN BEGIN
currentSubject := current_setting('hsadminng.assumedRoles'); currentSubject := current_setting('hsadminng.assumedRoles');
EXCEPTION WHEN OTHERS THEN EXCEPTION WHEN OTHERS THEN
RETURN NULL; RETURN ARRAY[]::varchar[];
END; END;
IF (currentSubject = '') THEN IF (currentSubject = '') THEN
RETURN NULL; RETURN ARRAY[]::varchar[];
END IF; END IF;
RETURN string_to_array(currentSubject, ';'); RETURN string_to_array(currentSubject, ';');
END; $$; END; $$;
-- ROLLBACK; ROLLBACK;
SET SESSION AUTHORIZATION DEFAULT; SET SESSION AUTHORIZATION DEFAULT;
CREATE OR REPLACE FUNCTION currentSubjectIds() CREATE OR REPLACE FUNCTION currentSubjectIds()
RETURNS uuid[] RETURNS uuid[]
@ -641,17 +642,36 @@ DECLARE
BEGIN BEGIN
currentUserId := currentUserId(); currentUserId := currentUserId();
assumedRoles := assumedRoles(); assumedRoles := assumedRoles();
IF ( assumedRoles IS NULL ) THEN IF ( CARDINALITY(assumedRoles) = 0 ) THEN
RETURN currentUserId; RETURN ARRAY[currentUserId];
END IF; END IF;
RAISE NOTICE 'assuming roles: %', assumedRoles; RAISE NOTICE 'assuming roles: %', assumedRoles;
SELECT ARRAY_AGG(uuid) FROM RbacRole WHERE name = ANY(assumedRoles) INTO assumedRoleIds; SELECT ARRAY_AGG(uuid) FROM RbacRole WHERE name = ANY(assumedRoles) INTO assumedRoleIds;
FOREACH assumedRoleId IN ARRAY assumedRoleIds LOOP IF assumedRoleIds IS NOT NULL THEN
IF ( NOT isGranted(currentUserId, assumedRoleId) ) THEN FOREACH assumedRoleId IN ARRAY assumedRoleIds LOOP
RAISE EXCEPTION 'user % has no permission to assume role %', currentUser(), assumedRoleId; IF ( NOT isGranted(currentUserId, assumedRoleId) ) THEN
END IF; RAISE EXCEPTION 'user % has no permission to assume role %', currentUser(), assumedRoleId;
END LOOP; END IF;
END LOOP;
END IF;
RETURN assumedRoleIds; RETURN assumedRoleIds;
END; $$; END; $$;
rollback;
set session authorization default;
CREATE OR REPLACE FUNCTION maxGrantDepth()
RETURNS integer
STABLE LEAKPROOF
LANGUAGE plpgsql AS $$
DECLARE
maxGrantDepth VARCHAR(63);
BEGIN
BEGIN
maxGrantDepth := current_setting('hsadminng.maxGrantDepth');
EXCEPTION WHEN OTHERS THEN
maxGrantDepth := NULL;
END;
RETURN coalesce(maxGrantDepth, '8')::integer;
END; $$;

View File

@ -38,7 +38,7 @@ BEGIN
-- ... also a customer admin role is created and granted to the customer owner role -- ... also a customer admin role is created and granted to the customer owner role
customerAdminRoleId = createRole('customer#'||NEW.prefix||'.admin'); customerAdminRoleId = createRole('customer#'||NEW.prefix||'.admin');
call grantRoleToRole(customerAdminRoleId, customerOwnerRoleId); call grantRoleToRole(customerAdminRoleId, customerOwnerRoleId, false);
-- ... to which a permission with view and add- ops is assigned -- ... to which a permission with view and add- ops is assigned
call grantPermissionsToRole(customerAdminRoleId, call grantPermissionsToRole(customerAdminRoleId,
createPermissions(forObjectUuid => NEW.uuid, permitOps => ARRAY['view', 'add-package'])); createPermissions(forObjectUuid => NEW.uuid, permitOps => ARRAY['view', 'add-package']));
@ -88,12 +88,11 @@ CREATE TRIGGER deleteRbacRulesForCustomer_Trigger
SET SESSION SESSION AUTHORIZATION DEFAULT; SET SESSION SESSION AUTHORIZATION DEFAULT;
ALTER TABLE customer ENABLE ROW LEVEL SECURITY; ALTER TABLE customer ENABLE ROW LEVEL SECURITY;
DROP VIEW IF EXISTS cust_view;
DROP VIEW IF EXISTS customer_rv; DROP VIEW IF EXISTS customer_rv;
CREATE OR REPLACE VIEW customer_rv AS CREATE OR REPLACE VIEW customer_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM customer AS target FROM customer AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', currentSubjectIds()) AS allowedObjId JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', 'customer', currentSubjectIds()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
GRANT ALL PRIVILEGES ON customer_rv TO restricted; GRANT ALL PRIVILEGES ON customer_rv TO restricted;

View File

@ -78,7 +78,7 @@ DROP VIEW IF EXISTS package_rv;
CREATE OR REPLACE VIEW package_rv AS CREATE OR REPLACE VIEW package_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM package AS target FROM package AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', currentSubjectIds()) AS allowedObjId JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', 'package', currentSubjectIds()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
GRANT ALL PRIVILEGES ON package_rv TO restricted; GRANT ALL PRIVILEGES ON package_rv TO restricted;

View File

@ -71,7 +71,7 @@ DROP VIEW IF EXISTS unixuser_rv;
CREATE OR REPLACE VIEW unixuser_rv AS CREATE OR REPLACE VIEW unixuser_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM unixuser AS target FROM unixuser AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', currentSubjectIds()) AS allowedObjId JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', 'unixuser', currentSubjectIds()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
GRANT ALL PRIVILEGES ON unixuser_rv TO restricted; GRANT ALL PRIVILEGES ON unixuser_rv TO restricted;

View File

@ -56,7 +56,7 @@ DROP VIEW IF EXISTS domain_rv;
CREATE OR REPLACE VIEW domain_rv AS CREATE OR REPLACE VIEW domain_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM Domain AS target FROM Domain AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', currentSubjectIds()) AS allowedObjId JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', 'domain', currentSubjectIds()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
GRANT ALL PRIVILEGES ON domain_rv TO restricted; GRANT ALL PRIVILEGES ON domain_rv TO restricted;

View File

@ -81,7 +81,7 @@ DROP VIEW IF EXISTS EMailAddress_rv;
CREATE OR REPLACE VIEW EMailAddress_rv AS CREATE OR REPLACE VIEW EMailAddress_rv AS
SELECT DISTINCT target.* SELECT DISTINCT target.*
FROM EMailAddress AS target FROM EMailAddress AS target
JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', currentSubjectIds()) AS allowedObjId JOIN queryAccessibleObjectUuidsOfSubjectIds( 'view', 'emailaddress', currentSubjectIds()) AS allowedObjId
ON target.uuid = allowedObjId; ON target.uuid = allowedObjId;
GRANT ALL PRIVILEGES ON EMailAddress_rv TO restricted; GRANT ALL PRIVILEGES ON EMailAddress_rv TO restricted;

87
sql/28-hs-tests.sql Normal file
View File

@ -0,0 +1,87 @@
-- hostmaster listing all customers
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
SET LOCAL hsadminng.assumedRoles = '';
SELECT * FROM customer_rv;
END TRANSACTION;
-- customer admin listing all their packages
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'admin@aae.example.com';
SET LOCAL hsadminng.assumedRoles = '';
SELECT * FROM package_rv;
END TRANSACTION;
-- cutomer admin listing all their unix users
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'admin@aae.example.com';
SET LOCAL hsadminng.assumedRoles = '';
SELECT * FROM unixuser_rv;
END TRANSACTION;
-- hostsharing admin assuming customer role and listing all accessible packages
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
SET LOCAL hsadminng.assumedRoles = 'customer#bbb.admin;customer#bbc.admin';
SELECT * FROM package_rv p;
END TRANSACTION;
-- hostsharing admin assuming two customer admin role and listing all accessible unixusers
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
SET LOCAL hsadminng.assumedRoles = 'customer#bbb.admin;customer#bbc.admin';
SELECT c.prefix, c.reference, uu.*
FROM unixuser_rv uu
JOIN package_rv p ON p.uuid = uu.packageuuid
JOIN customer_rv c ON c.uuid = p.customeruuid;
END TRANSACTION;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
SET LOCAL hsadminng.assumedRoles = 'customer#bbb.admin;customer#bbc.admin';
SELECT p.name, uu.name, dom.name
FROM domain_rv dom
JOIN unixuser_rv uu ON uu.uuid = dom.unixuseruuid
JOIN package_rv p ON p.uuid = uu.packageuuid
JOIN customer_rv c ON c.uuid = p.customeruuid;
END TRANSACTION;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
SET LOCAL hsadminng.assumedRoles = 'customer#bbb.admin;customer#bbc.admin';
-- TODO: we need tenant roles on parent objects
-- SET LOCAL hsadminng.assumedRoles = 'package#bbb03.owner;package#bbb08.owner';
SELECT p.name as "package", ema.localPart || '@' || dom.name as "email-address"
FROM emailaddress_rv ema
JOIN domain_rv dom ON dom.uuid = ema.domainuuid
JOIN unixuser_rv uu ON uu.uuid = dom.unixuseruuid
JOIN package_rv p ON p.uuid = uu.packageuuid
JOIN customer_rv c ON c.uuid = p.customeruuid;
END TRANSACTION;
ROLLBACK;
BEGIN TRANSACTION;
SET SESSION SESSION AUTHORIZATION restricted;
SET LOCAL hsadminng.currentUser = 'mike@hostsharing.net';
select * from customer_rv c where c.prefix='bbb';
END TRANSACTION;