--liquibase formatted sql

-- ============================================================================
--changeset michael.hoennig:rbac-base-REFERENCE endDelimiter:--//
-- ----------------------------------------------------------------------------

create type rbac.ReferenceType as enum ('rbac.subject', 'rbac.role', 'rbac.permission');

create table rbac.reference
(
    uuid uuid unique default uuid_generate_v4(),
    type rbac.ReferenceType not null
);

create or replace function rbac.assertReferenceType(argument varchar, referenceId uuid, expectedType rbac.ReferenceType)
    returns rbac.ReferenceType
    language plpgsql as $$
declare
    actualType rbac.ReferenceType;
begin
    if referenceId is null then
        raise exception '% must be a % and not null', argument, expectedType;
    end if;

    actualType = (select type from  rbac.reference where uuid = referenceId);
    if (actualType <> expectedType) then
        raise exception '% must reference a %, but got a %', argument, expectedType, actualType;
    end if;
    return expectedType;
end; $$;
--//

-- ============================================================================
--changeset michael.hoennig:rbac-base-SUBJECT runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
DO $$
    BEGIN
        IF NOT EXISTS (SELECT FROM information_schema.tables
                              WHERE table_schema = 'rbac' AND table_name = 'subject') THEN

            CREATE TABLE rbac.subject
            (
                uuid uuid primary key references rbac.reference (uuid) on delete cascade,
                name varchar(63) not null unique
            );

            CALL base.create_journal('rbac.subject');
        END IF;
    END
$$;

create or replace function rbac.create_subject(subjectName varchar)
    returns uuid
    returns null on null input
    language plpgsql as $$
declare
    stableUuidNamespace uuid;
    subjectUuid uuid;
begin
    stableUuidNamespace := '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid;
    subjectUuid := uuid_generate_v5(stableUuidNamespace, subjectName);
    insert
        into rbac.reference (uuid, type)
        values (subjectUuid, 'rbac.subject');
    insert
        into rbac.subject (uuid, name)
        values (subjectUuid, subjectName);
    return subjectUuid;
end;
$$;

create or replace function rbac.create_subject(refUuid uuid, subjectName varchar)
    returns uuid
    called on null input
    language plpgsql as $$
begin
    insert
        into  rbac.reference as r (uuid, type)
        values (coalesce(refUuid, uuid_generate_v4()), 'rbac.subject')
        returning r.uuid into refUuid;
    insert
        into rbac.subject (uuid, name)
        values (refUuid, subjectName);
    return refUuid;
end;
$$;

create or replace function rbac.find_subject_id(subjectName varchar)
    returns uuid
    returns null on null input
    language sql as $$
select uuid from rbac.subject where name = subjectName
$$;
--//

-- ============================================================================
--changeset michael.hoennig:rbac-base-OBJECT endDelimiter:--//
-- ----------------------------------------------------------------------------
/*

 */
create table rbac.object
(
    uuid        uuid primary key default uuid_generate_v4(),
    serialId    serial, -- TODO.perf: only needed for reverse deletion of temp test data
    objectTable varchar(64) not null,
    unique (objectTable, uuid)
);

call base.create_journal('rbac.object');

--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-GENERATE-RELATED-OBJECT endDelimiter:--//
-- ----------------------------------------------------------------------------

/*
    Inserts related rbac.object for use in the BEFORE INSERT TRIGGERs on the business objects.
 */
create or replace function rbac.insert_related_object()
    returns trigger
    language plpgsql
    strict as $$
declare
    objectUuid uuid;
    tableSchemaAndName text;
begin
    tableSchemaAndName := base.combine_table_schema_and_name(TG_TABLE_SCHEMA, TG_TABLE_NAME);
    if TG_OP = 'INSERT' then
        if NEW.uuid is null then
            insert
                into rbac.object (objectTable)
                values (tableSchemaAndName)
                returning uuid into objectUuid;
            NEW.uuid = objectUuid;
        else
            insert
                into rbac.object (uuid, objectTable)
                values (NEW.uuid, tableSchemaAndName)
                returning uuid into objectUuid;
        end if;
        return NEW;
    else
        raise exception 'invalid usage of TRIGGER AFTER INSERT';
    end if;
end; $$;

/*
    Deletes related rbac.object for use in the BEFORE DELETE TRIGGERs on the business objects.
    Through cascades all related rbac roles and grants are going to be deleted as well.
 */
create or replace function rbac.delete_related_rbac_rules_tf()
    returns trigger
    language plpgsql
    strict as $$
begin
    if TG_OP = 'DELETE' then
        delete from rbac.object where rbac.object.uuid = old.uuid;
    else
        raise exception 'invalid usage of TRIGGER BEFORE DELETE';
    end if;
    return old;
end; $$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-ROLE endDelimiter:--//
-- ----------------------------------------------------------------------------

create type rbac.RoleType as enum ('OWNER', 'ADMIN', 'AGENT', 'TENANT', 'GUEST', 'REFERRER');

create table rbac.role
(
    uuid       uuid primary key references rbac.reference (uuid) on delete cascade initially deferred, -- initially deferred
    objectUuid uuid not null references rbac.object (uuid) initially deferred,
    roleType   rbac.RoleType not null,
    unique (objectUuid, roleType)
);

call base.create_journal('rbac.role');
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-ROLE-DESCRIPTOR endDelimiter:--//
-- ----------------------------------------------------------------------------

create type rbac.RoleDescriptor as
(
    objectTable varchar(63), -- for human readability and easier debugging
    objectUuid  uuid,
    roleType    rbac.RoleType,
    assumed     boolean
);

create or replace function rbac.assumed()
    returns boolean
    stable -- leakproof
    language sql as $$
        select true;
$$;

create or replace function rbac.unassumed()
    returns boolean
    stable -- leakproof
    language sql as $$
select false;
$$;

create or replace function rbac.roleDescriptorOf(
        objectTable varchar(63), objectUuid uuid, roleType rbac.RoleType,
        assumed boolean = true) -- just for DSL readability, belongs actually to the grant
    returns rbac.RoleDescriptor
    returns null on null input
    stable -- leakproof
    language sql as $$
        select objectTable, objectUuid, roleType::rbac.RoleType, assumed;
$$;

create or replace function rbac.createRole(roleDescriptor rbac.RoleDescriptor)
    returns uuid
    returns null on null input
    language plpgsql as $$
declare
    referenceId uuid;
begin
    insert
        into  rbac.reference (type)
        values ('rbac.role')
        returning uuid into referenceId;
    insert
        into rbac.role (uuid, objectUuid, roleType)
        values (referenceId, roleDescriptor.objectUuid, roleDescriptor.roleType);
    return referenceId;
end;
$$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-IDNAME-FUNCTIONS endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace function rbac.findObjectUuidByIdName(objectTable varchar, objectIdName varchar)
    returns uuid
    returns null on null input
    language plpgsql as $$
declare
    sql  varchar;
    uuid uuid;
begin
    objectTable := base.pureIdentifier(objectTable);
    objectIdName := base.pureIdentifier(objectIdName);
    sql := format('select * from %s_uuid_by_id_name(%L);', objectTable, objectIdName);
    begin
        execute sql into uuid;
    exception
        when others then
            raise exception 'function %_uuid_by_id_name(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %\nSQL:%',
                objectTable, objectIdName, SQLERRM, SQLSTATE, objectTable, sql;
    end;
    if uuid is null then
        raise exception 'SQL returned null: %', sql;
    else
        return uuid;
    end if;
end ; $$;

create or replace function rbac.findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
    returns varchar
    returns null on null input
    language plpgsql as $$
declare
    sql    varchar;
    idName varchar;
begin
    objectTable := base.pureIdentifier(objectTable);
    sql := format('select * from %s_id_name_by_uuid(%L::uuid);', objectTable, objectUuid);
    begin
        execute sql into idName;
    exception
        when others then
            raise exception 'function %_id_name_by_uuid(''%'') failed: %, SQLSTATE: %. If the function itself could not be found, add identity view support to %',
                objectTable, objectUuid, SQLERRM, SQLSTATE, objectTable;
    end;
    return idName;
end ; $$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-ROLE-FUNCTIONS endDelimiter:--//
-- ----------------------------------------------------------------------------

create or replace procedure rbac.deleteRole(roleUUid uuid)
    language plpgsql as $$
begin
    --raise exception '% deleting role uuid %', rbac.currentSubjectOrAssumedRolesUuids(), roleUUid;
    delete from rbac.role where uuid = roleUUid;
end;
$$;

create or replace function rbac.findRoleId(roleIdName varchar)
    returns uuid
    returns null on null input
    language plpgsql as $$
declare
    roleParts                 text;
    roleTypeFromRoleIdName    rbac.RoleType;
    objectNameFromRoleIdName  text;
    objectTableFromRoleIdName text;
    objectUuidOfRole          uuid;
    roleUuid                  uuid;
begin
    -- TODO.refa: extract function rbac.toRoleDescriptor(roleIdName varchar) + find other occurrences
    roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), ':'));
    objectTableFromRoleIdName = split_part(roleParts, '#', 1);
    objectNameFromRoleIdName = split_part(roleParts, '#', 2);
    roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
    objectUuidOfRole = rbac.findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName);

    select uuid
        from rbac.role
        where objectUuid = objectUuidOfRole
          and roleType = roleTypeFromRoleIdName
        into roleUuid;
    return roleUuid;
end; $$;

create or replace function rbac.findRoleId(roleDescriptor rbac.RoleDescriptor)
    returns uuid
    returns null on null input
    language sql as $$
select uuid from rbac.role where objectUuid = roleDescriptor.objectUuid and roleType = roleDescriptor.roleType;
$$;

create or replace function rbac.getRoleId(roleDescriptor rbac.RoleDescriptor)
    returns uuid
    language plpgsql as $$
declare
    roleUuid uuid;
begin
    assert roleDescriptor is not null, 'roleDescriptor must not be null';

    roleUuid := rbac.findRoleId(roleDescriptor);
    if (roleUuid is null) then
        raise exception 'rbac.role "%#%.%" not found', roleDescriptor.objectTable, roleDescriptor.objectUuid, roleDescriptor.roleType;
    end if;
    return roleUuid;
end;
$$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-BEFORE-DELETE-ROLE-TRIGGER endDelimiter:--//
-- ----------------------------------------------------------------------------

/*
    rbac.role BEFORE DELETE TRIGGER function which deletes all related roles.
 */
create or replace function rbac.delete_grants_of_role_tf()
    returns trigger
    language plpgsql
    strict as $$
begin
    if TG_OP = 'DELETE' then
        delete from rbac.grant g where old.uuid in (g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid);
    else
        raise exception 'invalid usage of TRIGGER BEFORE DELETE';
    end if;
    return old;
end; $$;

/*
    Installs the rbac.role BEFORE DELETE TRIGGER.
 */
create trigger delete_grants_of_role_tg
    before delete
    on rbac.role
    for each row
execute procedure rbac.delete_grants_of_role_tf();
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-BEFORE-DELETE-OBJECT-TRIGGER endDelimiter:--//
-- ----------------------------------------------------------------------------

/*
    rbac.object BEFORE DELETE TRIGGER function which deletes all related roles.
 */
create or replace function rbac.delete_roles_of_object_tf()
    returns trigger
    language plpgsql
    strict as $$
begin
    if TG_OP = 'DELETE' then
        delete from rbac.permission p where p.objectuuid = old.uuid;
        delete from rbac.role r where r.objectUuid = old.uuid;
    else
        raise exception 'invalid usage of TRIGGER BEFORE DELETE';
    end if;
    return old;
end; $$;

/*
    Installs the rbac.role BEFORE DELETE TRIGGER.
 */
create trigger delete_roles_of_object_tg
    before delete
    on rbac.object
    for each row
        execute procedure rbac.delete_roles_of_object_tf();
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-PERMISSION endDelimiter:--//
-- ----------------------------------------------------------------------------
create domain rbac.RbacOp as varchar(6)
    check (
               VALUE = 'DELETE'
            or VALUE = 'UPDATE'
            or VALUE = 'SELECT'
            or VALUE = 'INSERT'
            or VALUE = 'ASSUME'
        );

create table rbac.permission
(
    uuid        uuid primary key references  rbac.reference (uuid) on delete cascade,
    objectUuid  uuid not null references rbac.object,
    op          rbac.RbacOp not null,
    opTableName varchar(60)
);
-- TODO.perf: check if these indexes are really useful
create index on rbac.permission (objectUuid, op);
create index on rbac.permission (opTableName, op);

ALTER TABLE rbac.permission
    ADD CONSTRAINT unique_including_null_values UNIQUE NULLS NOT DISTINCT (objectUuid, op, opTableName);

call base.create_journal('rbac.permission');

create or replace function rbac.createPermission(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
    returns uuid
    language plpgsql as $$
declare
    permissionUuid uuid;
begin
    if (forObjectUuid is null) then
        raise exception 'forObjectUuid must not be null';
    end if;
    if (forOp = 'INSERT' and forOpTableName is null) then
        raise exception 'INSERT permissions needs forOpTableName';
    end if;
    if (forOp <> 'INSERT' and forOpTableName is not null) then
        raise exception 'forOpTableName must only be specified for ops: [INSERT]'; -- currently no other
    end if;

    permissionUuid := (
        select uuid from rbac.permission
         where objectUuid = forObjectUuid
           and op = forOp and opTableName is not distinct from forOpTableName);
    if (permissionUuid is null) then
        insert into  rbac.reference ("type")
            values ('rbac.permission')
            returning uuid into permissionUuid;
        begin
            insert into rbac.permission (uuid, objectUuid, op, opTableName)
                values (permissionUuid, forObjectUuid, forOp, forOpTableName);
        exception
            when others then
                raise exception 'insert into rbac.permission (uuid, objectUuid, op, opTableName)
                values (%, %, %, %);', permissionUuid, forObjectUuid, forOp, forOpTableName;
        end;
    end if;
    return permissionUuid;
end; $$;

create or replace function rbac.findEffectivePermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
    returns uuid
    returns null on null input
    stable -- leakproof
    language sql as $$
select uuid
    from rbac.permission p
    where p.objectUuid = forObjectUuid
      and (forOp = 'SELECT' or p.op = forOp) -- all other rbac.RbacOp include 'SELECT'
      and p.opTableName = forOpTableName
$$;

create or replace function rbac.findPermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
    returns uuid
    returns null on null input
    stable -- leakproof
    language sql as $$
select uuid
    from rbac.permission p
    where p.objectUuid = forObjectUuid
      and p.op = forOp
      and p.opTableName = forOpTableName
$$;

create or replace function rbac.getPermissionId(forObjectUuid uuid, forOp rbac.RbacOp, forOpTableName text = null)
    returns uuid
    stable -- leakproof
    language plpgsql as $$
declare
    permissionUuid uuid;
begin
    select uuid into permissionUuid
        from rbac.permission p
        where p.objectUuid = forObjectUuid
          and p.op = forOp
          and forOpTableName is null or p.opTableName = forOpTableName;
    assert permissionUuid is not null,
        format('permission %s %s for object UUID %s cannot be found', forOp, forOpTableName, forObjectUuid);
    return permissionUuid;
end; $$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-duplicate-role-grant-exception endDelimiter:--//
-- ----------------------------------------------------------------------------

create or replace procedure rbac.raiseDuplicateRoleGrantException(subRoleId uuid, superRoleId uuid)
    language plpgsql as $$
declare
    subRoleIdName text;
    superRoleIdName text;
begin
    select roleIdName from rbac.role_ev where uuid=subRoleId into subRoleIdName;
    select roleIdName from rbac.role_ev where uuid=superRoleId into superRoleIdName;
    raise exception '[400] Duplicate role grant detected: role % (%) already granted to % (%)', subRoleId, subRoleIdName, superRoleId, superRoleIdName;
end;
$$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-GRANTS  endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Table to store grants / role- or permission assignments to subjects or roles.
 */
create table rbac.grant
(
    uuid                uuid primary key default uuid_generate_v4(),
    grantedByTriggerOf  uuid references rbac.object (uuid) on delete cascade initially deferred ,
    grantedByRoleUuid   uuid references rbac.role (uuid),
    ascendantUuid       uuid references  rbac.reference (uuid),
    descendantUuid      uuid references  rbac.reference (uuid),
    assumed             boolean not null default true,  -- auto assumed (true) vs. needs assumeRoles (false)
    unique (ascendantUuid, descendantUuid),
    constraint rbacGrant_createdBy check ( grantedByRoleUuid is null or grantedByTriggerOf is null) );
create index on rbac.grant (ascendantUuid);
create index on rbac.grant (descendantUuid);

call base.create_journal('rbac.grant');
create or replace function rbac.findGrantees(grantedId uuid)
    returns setof  rbac.reference
    returns null on null input
    language sql as $$
with recursive grants as (
    select descendantUuid, ascendantUuid
        from rbac.grant
        where descendantUuid = grantedId
    union all
    select g.descendantUuid, g.ascendantUuid
        from rbac.grant g
                 inner join grants on grants.ascendantUuid = g.descendantUuid
)
select ref.*
    from grants
             join rbac.reference ref on ref.uuid = grants.ascendantUuid;
$$;

create or replace function rbac.isGranted(granteeIds uuid[], grantedId uuid)
    returns bool
    returns null on null input
    language sql as $$
with recursive grants as (
    select descendantUuid, ascendantUuid
        from rbac.grant
        where descendantUuid = grantedId
    union all
    select "grant".descendantUuid, "grant".ascendantUuid
        from rbac.grant "grant"
                 inner join grants recur on recur.ascendantUuid = "grant".descendantUuid
)
select exists (
    select true
        from grants
        where ascendantUuid = any(granteeIds)
) or grantedId = any(granteeIds);
$$;

create or replace function rbac.isGranted(granteeId uuid, grantedId uuid)
    returns bool
    returns null on null input
    language sql as $$
select * from rbac.isGranted(array[granteeId], grantedId);
$$;
create or replace function rbac.isPermissionGrantedToSubject(permissionId uuid, subjectId uuid)
    returns BOOL
    stable -- leakproof
    language sql as $$
with recursive grants as (
    select descendantUuid, ascendantUuid
        from rbac.grant
        where descendantUuid = permissionId
    union all
    select g.descendantUuid, g.ascendantUuid
        from rbac.grant g
                 inner join grants on grants.ascendantUuid = g.descendantUuid
)
select exists(
    select true
        from grants
        where ascendantUuid = subjectId
);
$$;

create or replace function rbac.hasInsertPermission(objectUuid uuid, tableName text )
    returns BOOL
    stable -- leakproof
    language plpgsql as $$
declare
    permissionUuid uuid;
begin
    permissionUuid = rbac.findPermissionId(objectUuid, 'INSERT'::rbac.RbacOp, tableName);
    return permissionUuid is not null;
end;
$$;

create or replace function rbac.hasGlobalRoleGranted(forAscendantUuid uuid)
    returns bool
    stable -- leakproof
    language sql as $$
select exists(
           select r.uuid
               from rbac.grant as g
                        join rbac.role as r on r.uuid = g.descendantuuid
                        join rbac.object as o on o.uuid = r.objectuuid
               where g.ascendantuuid = forAscendantUuid
                 and o.objecttable = 'rbac.global'
           );
$$;

create or replace procedure rbac.grantPermissionToRole(permissionUuid uuid, roleUuid uuid)
    language plpgsql as $$
begin
    perform rbac.assertReferenceType('roleId (ascendant)', roleUuid, 'rbac.role');
    perform rbac.assertReferenceType('permissionId (descendant)', permissionUuid, 'rbac.permission');

    insert
        into rbac.grant (grantedByTriggerOf, ascendantUuid, descendantUuid, assumed)
        values (rbac.currentTriggerObjectUuid(), roleUuid, permissionUuid, true)
    on conflict do nothing; -- allow granting multiple times
end;
$$;

create or replace procedure rbac.grantPermissionToRole(permissionUuid uuid, roleDesc rbac.RoleDescriptor)
    language plpgsql as $$
begin
    call rbac.grantPermissionToRole(permissionUuid, rbac.findRoleId(roleDesc));
end;
$$;

create or replace procedure rbac.grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true)
    language plpgsql as $$
begin
    perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
    perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');

    if rbac.isGranted(subRoleId, superRoleId) then
        call rbac.raiseDuplicateRoleGrantException(subRoleId, superRoleId);
    end if;

    insert
        into rbac.grant (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
        values (rbac.currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
    on conflict do nothing; -- allow granting multiple times
end; $$;


create or replace procedure rbac.grantRoleToRole(subRole rbac.RoleDescriptor, superRole rbac.RoleDescriptor, doAssume bool = true)
    language plpgsql as $$
declare
    superRoleId uuid;
    subRoleId uuid;
begin
    -- TODO.refa: maybe separate method rbac.grantRoleToRoleIfNotNull(...) for NULLABLE references
    if superRole.objectUuid is null or subRole.objectuuid is null then
        return;
    end if;

    superRoleId := rbac.findRoleId(superRole);
    subRoleId := rbac.findRoleId(subRole);

    perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
    perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');

    if rbac.isGranted(subRoleId, superRoleId) then
        call rbac.raiseDuplicateRoleGrantException(subRoleId, superRoleId);
    end if;

    insert
        into rbac.grant (grantedByTriggerOf, ascendantuuid, descendantUuid, assumed)
        values (rbac.currentTriggerObjectUuid(), superRoleId, subRoleId, doAssume)
    on conflict do nothing; -- allow granting multiple times
end; $$;

create or replace procedure rbac.revokeRoleFromRole(subRole rbac.RoleDescriptor, superRole rbac.RoleDescriptor)
    language plpgsql as $$
declare
    superRoleId uuid;
    subRoleId uuid;
begin
    superRoleId := rbac.findRoleId(superRole);
    subRoleId := rbac.findRoleId(subRole);

    perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
    perform rbac.assertReferenceType('subRoleId (descendant)', subRoleId, 'rbac.role');

    if (rbac.isGranted(superRoleId, subRoleId)) then
        delete from rbac.grant where ascendantUuid = superRoleId and descendantUuid = subRoleId;
    else
        raise exception 'cannot revoke role % (%) from % (%) because it is not granted',
            subRole, subRoleId, superRole, superRoleId;
    end if;
end; $$;

create or replace procedure rbac.revokePermissionFromRole(permissionId UUID, superRole rbac.RoleDescriptor)
    language plpgsql as $$
declare
    superRoleId uuid;
    permissionOp text;
    objectTable text;
    objectUuid uuid;
begin
    superRoleId := rbac.findRoleId(superRole);

    perform rbac.assertReferenceType('superRoleId (ascendant)', superRoleId, 'rbac.role');
    perform rbac.assertReferenceType('permission (descendant)', permissionId, 'rbac.permission');

    if (rbac.isGranted(superRoleId, permissionId)) then
        delete from rbac.grant where ascendantUuid = superRoleId and descendantUuid = permissionId;
    else
        select p.op, o.objectTable, o.uuid
            from rbac.grant g
                     join rbac.permission p on p.uuid=g.descendantUuid
                     join rbac.object o on o.uuid=p.objectUuid
            where g.uuid=permissionId
            into permissionOp, objectTable, objectUuid;

        raise exception 'cannot revoke permission % (% on %#% (%) from % (%)) because it is not granted',
            permissionId, permissionOp, objectTable, objectUuid, permissionId, superRole, superRoleId;
    end if;
end; $$;

-- ============================================================================
--changeset michael.hoennig:rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS runOnChange=true endDelimiter:--//
-- ----------------------------------------------------------------------------
/*

 */
create or replace function rbac.queryAccessibleObjectUuidsOfSubjectIds(
    requiredOp rbac.RbacOp,
    forObjectTable varchar,
    subjectIds uuid[],
    maxObjects integer = 8000)
    returns setof uuid
    returns null on null input
    language plpgsql as $$
declare
    foundRows bigint;
begin
    return query
        WITH RECURSIVE grants AS (
            SELECT descendantUuid, ascendantUuid, 1 AS level
                FROM rbac.grant
                WHERE assumed
                  AND ascendantUuid = any(subjectIds)
            UNION ALL
            SELECT g.descendantUuid, g.ascendantUuid, grants.level + 1 AS level
                FROM rbac.grant g
                         INNER JOIN grants ON grants.descendantUuid = g.ascendantUuid
                WHERE g.assumed
        ),
       granted AS (
           SELECT DISTINCT descendantUuid
               FROM grants
       )
        SELECT DISTINCT perm.objectUuid
            FROM granted
                     JOIN rbac.permission perm ON granted.descendantUuid = perm.uuid
                     JOIN rbac.object obj ON obj.uuid = perm.objectUuid
            WHERE (requiredOp = 'SELECT' OR perm.op = requiredOp)
              AND obj.objectTable = forObjectTable
            LIMIT maxObjects+1;

    foundRows = base.lastRowCount();
    if foundRows > maxObjects then
        raise exception '[400] Too many accessible objects, limit is %, found %.', maxObjects, foundRows
            using
                errcode = 'P0003',
                hint = 'Please assume a sub-role and try again.';
    end if;
end;
$$;
--//

-- ============================================================================
--changeset michael.hoennig:rbac-base-QUERY-GRANTED-PERMISSIONS endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Returns all permissions accessible to the given subject UUID (subject or role).
 */
create or replace function rbac.queryPermissionsGrantedToSubjectId(subjectId uuid)
    returns setof rbac.permission
    strict
    language sql as $$
with recursive grants as (
    select descendantUuid, ascendantUuid
        from rbac.grant
        where ascendantUuid = subjectId
    union all
    select g.descendantUuid, g.ascendantUuid
        from rbac.grant g
                 inner join grants on grants.descendantUuid = g.ascendantUuid
)
select perm.*
    from rbac.permission perm
    where perm.uuid in (
        select descendantUuid
            from grants
    );
$$;

--//

-- ============================================================================
--changeset michael.hoennig:rbac-base-QUERY-SUBJECTS-WITH-PERMISSION-FOR-OBJECT endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Returns all subject UUIDs which have any permission for the given object UUID.
 */

create or replace function rbac.queryAllRbacSubjectsWithPermissionsFor(objectId uuid)
    returns setof rbac.subject
    returns null on null input
    language sql as $$
select *
    from rbac.subject
    where uuid in (
        -- @formatter:off
        with recursive grants as (
            select descendantUuid, ascendantUuid
                from rbac.grant
                where descendantUuid = objectId
            union all
            select "grant".descendantUuid, "grant".ascendantUuid
                from rbac.grant "grant"
                inner join grants recur on recur.ascendantUuid = "grant".descendantUuid
        )
        -- @formatter:on
        select ascendantUuid
            from grants);
$$;
--//


-- ============================================================================
--changeset michael.hoennig:rbac-base-PGSQL-ROLES runOnChange:true validCheckSum:ANY context:!external-db endDelimiter:--//
-- ----------------------------------------------------------------------------

do $$
    begin
        if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then
            if not exists (select from pg_catalog.pg_roles where rolname = 'admin') then
                create role admin;
            end if;
            grant all privileges on all tables in schema public to admin;
        end if;

        if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then
            if not exists (select from pg_catalog.pg_roles where rolname = 'restricted') then
                create role restricted;
            end if;

            grant all privileges on all tables in schema public to restricted;
        end if;
    end $$;
--//
