--liquibase formatted sql


-- ============================================================================
--changeset michael.hoennig:rbac-context-DETERMINE endDelimiter:--//
-- ----------------------------------------------------------------------------

create or replace function rbac.determineCurrentSubjectUuid(currentSubject varchar)
    returns uuid
    stable -- leakproof
    language plpgsql as $$
declare
    currentSubjectUuid uuid;
begin
    if currentSubject = '' then
        return null;
    end if;

    select uuid from rbac.subject where name = currentSubject into currentSubjectUuid;
    if currentSubjectUuid is null then
        raise exception '[401] subject % given in `base.defineContext(...)` does not exist', currentSubject;
    end if;
    return currentSubjectUuid;
end; $$;

create or replace function rbac.determinecurrentsubjectorassumedrolesuuids(currentSubjectOrAssumedRolesUuids uuid, assumedRoles varchar)
    returns uuid[]
    stable -- leakproof
    language plpgsql as $$
declare
    roleName            text;
    roleNameParts       text;
    objectTableToAssume varchar(63);
    objectNameToAssume  varchar(63);
    objectUuidToAssume  uuid;
    roleTypeToAssume    rbac.RoleType;
    roleIdsToAssume     uuid[];
    roleUuidToAssume    uuid;
begin
    if currentSubjectOrAssumedRolesUuids is null then
        if length(coalesce(assumedRoles, '')) > 0 then
            raise exception '[403] undefined has no permission to assume role %', assumedRoles;
        else
            return array[]::uuid[];
        end if;
    end if;
    if  length(coalesce(assumedRoles, '')) = 0 then
        return array [currentSubjectOrAssumedRolesUuids];
    end if;

    foreach roleName in array string_to_array(assumedRoles, ';')
        loop
            roleNameParts = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), ':'));
            objectTableToAssume = split_part(roleNameParts, '#', 1);
            objectNameToAssume = split_part(roleNameParts, '#', 2);
            roleTypeToAssume = split_part(roleNameParts, '#', 3);

            objectUuidToAssume = rbac.findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
            if objectUuidToAssume is null then
                raise exception '[401] object % cannot be found in table % (from roleNameParts=%)', objectNameToAssume, objectTableToAssume, roleNameParts;
            end if;

            select uuid
                from rbac.role r
                where r.objectUuid = objectUuidToAssume
                  and r.roleType = roleTypeToAssume
                into roleUuidToAssume;
            if roleUuidToAssume is null then
                raise exception '[403] role % does not exist or is not accessible for subject %', roleName, base.currentSubject();
            end if;
            if not rbac.isGranted(currentSubjectOrAssumedRolesUuids, roleUuidToAssume) then
                raise exception '[403] subject % has no permission to assume role %', base.currentSubject(), roleName;
            end if;
            roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
        end loop;

    return roleIdsToAssume;
end; $$;

-- ============================================================================
--changeset michael.hoennig:rbac-context-CONTEXT-DEFINED endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Callback which is called after the context has been (re-) defined.
    This function will be overwritten by later changesets.
 */
create or replace procedure base.contextDefined(
    currentTask varchar(127),
    currentRequest text,
    currentSubject varchar(63),
    assumedRoles varchar(1023)
)
    language plpgsql as $$
declare
    currentSubjectUuid uuid;
begin
    execute format('set local hsadminng.currentTask to %L', currentTask);

    execute format('set local hsadminng.currentRequest to %L', currentRequest);

    execute format('set local hsadminng.currentSubject to %L', currentSubject);
    select rbac.determineCurrentSubjectUuid(currentSubject) into currentSubjectUuid;
    execute format('set local hsadminng.currentSubjectUuid to %L', coalesce(currentSubjectUuid::text, ''));

    execute format('set local hsadminng.assumedRoles to %L', assumedRoles);
    execute format('set local hsadminng.currentSubjectOrAssumedRolesUuids to %L',
       (select array_to_string(rbac.determinecurrentsubjectorassumedrolesuuids(currentSubjectUuid, assumedRoles), ';')));

    raise notice 'Context defined as: %, %, %, [%]', currentTask, currentRequest, currentSubject, assumedRoles;
end; $$;


-- ============================================================================
--changeset michael.hoennig:rbac-context-current-subject-ID endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Returns the uuid of the current subject as set via `base.defineContext(...)`.
 */

create or replace function rbac.currentSubjectUuid()
    returns uuid
    stable -- leakproof
    language plpgsql as $$
declare
    currentSubjectUuid text;
    currentSubjectName text;
begin
    begin
        currentSubjectUuid := current_setting('hsadminng.currentSubjectUuid');
    exception
        when others then
            currentSubjectUuid := null;
    end;
    if (currentSubjectUuid is null or currentSubjectUuid = '') then
        currentSubjectName := base.currentSubject();
        if (length(currentSubjectName) > 0) then
            raise exception '[401] currentSubjectUuid cannot be determined, unknown subject name "%"', currentSubjectName;
        else
            raise exception '[401] currentSubjectUuid cannot be determined, please call `base.defineContext(...)` first;"';
        end if;
    end if;
    return currentSubjectUuid::uuid;
end; $$;
--//

-- ============================================================================
--changeset michael.hoennig:rbac-context-CURRENT-SUBJECT-UUIDS endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
    Returns the uuid of the current subject as set via `base.defineContext(...)`,
    or, if any, the uuids of all assumed roles as set via `base.defineContext(...)`
    or empty array, if context is not defined.
 */
create or replace function rbac.currentSubjectOrAssumedRolesUuids()
    returns uuid[]
    stable -- leakproof
    language plpgsql as $$
declare
    currentSubjectOrAssumedRolesUuids text;
    currentSubjectName text;
begin
    begin
        currentSubjectOrAssumedRolesUuids := current_setting('hsadminng.currentSubjectOrAssumedRolesUuids');
    exception
        when others then
            currentSubjectOrAssumedRolesUuids := null;
    end;
    if (currentSubjectOrAssumedRolesUuids is null or length(currentSubjectOrAssumedRolesUuids) = 0 ) then
        currentSubjectName := base.currentSubject();
        if (length(currentSubjectName) > 0) then
            raise exception '[401] currentSubjectOrAssumedRolesUuids (%) cannot be determined, unknown subject name "%"', currentSubjectOrAssumedRolesUuids, currentSubjectName;
        else
            raise exception '[401] currentSubjectOrAssumedRolesUuids cannot be determined, please call `base.defineContext(...)` with a valid subject;"';
        end if;
    end if;
    return string_to_array(currentSubjectOrAssumedRolesUuids, ';');
end; $$;
--//