add RbacUserController/-Entity/-Repository

This commit is contained in:
Michael Hoennig 2022-08-04 17:19:45 +02:00
parent 18f3234272
commit 06996e4dc4
9 changed files with 427 additions and 220 deletions

View File

@ -0,0 +1,46 @@
package net.hostsharing.hsadminng.rbac.rbacuser;
import net.hostsharing.hsadminng.context.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional;
import java.util.ArrayList;
@RestController
public class RbacUserController {
@Autowired
private Context context;
@Autowired
private RbacUserRepository rbacUserRepository;
@GetMapping(value = "/api/rbacuser")
@Transactional
public Iterable<RbacUserEntity> listUsers(
@RequestHeader(name = "current-user") String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
@RequestParam(name="name", required = false) String userName
) {
context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
return rbacUserRepository.findByOptionalNameLike(userName);
}
@GetMapping(value = "/api/rbacuser/{userName}/permissions")
@Transactional
public Iterable<RbacUserPermission> listUserPermissions(
@RequestHeader(name = "current-user") String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) String assumedRoles,
@PathVariable(name= "userName") String userName
) {
context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
return rbacUserRepository.findPermissionsOfUser(userName);
}
}

View File

@ -0,0 +1,47 @@
package net.hostsharing.hsadminng.rbac.rbacuser;
import lombok.*;
import org.springframework.data.annotation.Immutable;
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "rbacuser_rv")
@Getter
@Setter
@ToString
@Immutable
@NoArgsConstructor
@AllArgsConstructor
//@SqlResultSetMapping(
// name = "rbacUserPermissionMapping",
// classes = {
// @ConstructorResult(
// targetClass = RbacUserPermission.class,
// columns = {
// @ColumnResult(name = "roleUuid", type = UUID.class),
// @ColumnResult(name = "oleName", type = String.class),
// @ColumnResult(name = "permissionUuid", type = UUID.class),
// @ColumnResult(name = "op", type=String.class),
// @ColumnResult(name = "objectTable", type=String.class),
// @ColumnResult(name = "objectIdName", type =String.class),
// @ColumnResult(name = "objectUuid", type = UUID.class),
// @ColumnResult(name = "campId", type = Integer.class),
// @ColumnResult(name = "userCount", type = Byte.class)
// }
// )
// }
//)
//@NamedNativeQuery(
// name = "grantedPermissions",
// query = "SELECT * FROM grantedPermissions(:userName)",
// resultSetMapping = "rbacUserPermissionMapping"
//)
public class RbacUserEntity {
@Id
private UUID uuid;
private String name;
}

View File

@ -0,0 +1,15 @@
package net.hostsharing.hsadminng.rbac.rbacuser;
import java.util.UUID;
public interface RbacUserPermission {
UUID getRoleUuid();
String getRoleName();
UUID getPermissionUuid();
String getOp();
String getObjectTable();
String getObjectIdName();
UUID getObjectUuid();
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.rbac.rbacuser;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.UUID;
public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
@Query("SELECT u FROM RbacUserEntity u WHERE :userName is null or u.name like concat(:userName, '%')")
List<RbacUserEntity> findByOptionalNameLike(final String userName);
@Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true)
Iterable<RbacUserPermission> findPermissionsOfUser(@Param("userName") String userName);
}

View File

@ -501,36 +501,37 @@ $$;
--changeset rbac-base-QUERY-GRANTED-PERMISSIONS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Returns all permissions accessible to the given subject UUID (user or role).
*/
create or replace function queryGrantedPermissionsOfSubjectIds(requiredOp RbacOp, subjectIds uuid[])
create or replace function queryPermissionsGrantedToSubjectId(subjectId uuid)
returns setof RbacPermission
strict
language sql as $$
select distinct *
-- @formatter:off
select *
from RbacPermission
where op = '*'
or op = requiredOp
and uuid in (with recursive grants as (select distinct descendantUuid,
ascendantUuid
from RbacGrants
where ascendantUuid = any (subjectIds)
union all
select "grant".descendantUuid,
"grant".ascendantUuid
from RbacGrants "grant"
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid)
select descendantUuid
from grants);
where uuid in (
with recursive grants as (
select distinct descendantUuid, ascendantUuid
from RbacGrants
where ascendantUuid = subjectId
union all
select "grant".descendantUuid, "grant".ascendantUuid
from RbacGrants "grant"
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid
)
select descendantUuid
from grants
);
-- @formatter:on
$$;
--//
-- ============================================================================
--changeset rbac-base-QUERY-USERS-WITH-PERMISSION-FOR-OBJECT:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Returns all user UUIDs which have any permission for the given object UUID.
*/
create or replace function queryAllRbacUsersWithPermissionsFor(objectId uuid)
@ -554,190 +555,6 @@ $$;
--//
-- ============================================================================
--changeset rbac-CURRENT-USER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
*/
create or replace function currentUser()
returns varchar(63)
stable leakproof
language plpgsql as $$
declare
currentUser varchar(63);
begin
begin
currentUser := current_setting('hsadminng.currentUser');
exception
when others then
currentUser := null;
end;
if (currentUser is null or currentUser = '') then
raise exception 'hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
end if;
return currentUser;
end; $$;
create or replace function currentUserId()
returns uuid
stable leakproof
language plpgsql as $$
declare
currentUser varchar(63);
currentUserId uuid;
begin
currentUser := currentUser();
currentUserId = (select uuid from RbacUser where name = currentUser);
if currentUserId is null then
raise exception 'hsadminng.currentUser defined as %, but does not exists', currentUser;
end if;
return currentUserId;
end; $$;
--//
-- ============================================================================
--changeset rbac-ASSUMED-ROLES:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
*/
create or replace function assumedRoles()
returns varchar(63)[]
stable leakproof
language plpgsql as $$
declare
currentSubject varchar(63);
begin
begin
currentSubject := current_setting('hsadminng.assumedRoles');
exception
when others then
return array []::varchar[];
end;
if (currentSubject = '') then
return array []::varchar[];
end if;
return string_to_array(currentSubject, ';');
end; $$;
create or replace function pureIdentifier(rawIdentifier varchar)
returns varchar
returns null on null input
language plpgsql as $$
begin
return regexp_replace(rawIdentifier, '\W+', '');
end; $$;
-- TODO: rename to findObjectUuidByIdName
create or replace function findUuidByIdName(objectTable varchar, objectIdName varchar)
returns uuid
returns null on null input
language plpgsql as $$
declare
sql varchar;
uuid uuid;
begin
objectTable := pureIdentifier(objectTable);
objectIdName := pureIdentifier(objectIdName);
sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName);
begin
raise notice 'sql: %', sql;
execute sql into uuid;
exception
when others then
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
end;
return uuid;
end ; $$;
create or replace function findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
returns varchar
returns null on null input
language plpgsql as $$
declare
sql varchar;
idName varchar;
begin
objectTable := pureIdentifier(objectTable);
sql := format('select * from %sIdNameByUuid(%L::uuid);', objectTable, objectUuid);
begin
raise notice 'sql: %', sql;
execute sql into idName;
exception
when others then
raise exception 'function %IdNameByUuid(...) not found, add identity view support for table %', objectTable, objectTable;
end;
return idName;
end ; $$;
create or replace function currentSubjects()
returns varchar(63)[]
stable leakproof
language plpgsql as $$
declare
assumedRoles varchar(63)[];
begin
assumedRoles := assumedRoles();
if array_length(assumedRoles(), 1) > 0 then
return assumedRoles();
else
return array [currentUser()]::varchar(63)[];
end if;
end; $$;
create or replace function currentSubjectIds()
returns uuid[]
stable leakproof
language plpgsql as $$
declare
currentUserId uuid;
roleNames varchar(63)[];
roleName varchar(63);
objectTableToAssume varchar(63);
objectNameToAssume varchar(63);
objectUuidToAssume uuid;
roleTypeToAssume RbacRoleType;
roleIdsToAssume uuid[];
roleUuidToAssume uuid;
begin
currentUserId := currentUserId();
if currentUserId is null then
raise exception 'user % does not exist', currentUser();
end if;
roleNames := assumedRoles();
if cardinality(roleNames) = 0 then
return array [currentUserId];
end if;
raise notice 'assuming roles: %', roleNames;
foreach roleName in array roleNames
loop
roleName = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), '.'));
objectTableToAssume = split_part(roleName, '#', 1);
objectNameToAssume = split_part(roleName, '#', 2);
roleTypeToAssume = split_part(roleName, '#', 3);
objectUuidToAssume = findUuidByIdName(objectTableToAssume, objectNameToAssume);
-- TODO: either the result needs to be cached at least per transaction or we need to get rid of SELCT in a loop
select uuid as roleuuidToAssume
from RbacRole r
where r.objectUuid = objectUuidToAssume
and r.roleType = roleTypeToAssume
into roleUuidToAssume;
if (not isGranted(currentUserId, roleUuidToAssume)) then
raise exception 'user % (%) has no permission to assume role % (%)', currentUser(), currentUserId, roleName, roleUuidToAssume;
end if;
roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
end loop;
return roleIdsToAssume;
end; $$;
--//
-- ============================================================================
--changeset rbac-base-PGSQL-ROLES:1 endDelimiter:--//
@ -750,21 +567,3 @@ create role restricted;
grant all privileges on all tables in schema public to restricted;
--//
-- ============================================================================
--changeset rbac-base-ROLE-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the role table with row-level limitation
based on the grants of the current user or assumed roles.
*/
drop view if exists rbacrole_rv;
create or replace view rbacrole_rv as
select DISTINCT r.*, o.objectTable,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
from rbacrole as r
join rbacobject as o on o.uuid=r.objectuuid
where isGranted(currentSubjectIds(), r.uuid);
grant all privileges on rbacrole_rv to restricted;
--//

View File

@ -0,0 +1,187 @@
--liquibase formatted sql
-- ============================================================================
--changeset rbac-current-CURRENT-USER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Returns the current user as set by `hsadminng.currentUser`.
Raises exception if not set.
*/
create or replace function currentUser()
returns varchar(63)
stable leakproof
language plpgsql as $$
declare
currentUser varchar(63);
begin
begin
currentUser := current_setting('hsadminng.currentUser');
exception
when others then
currentUser := null;
end;
if (currentUser is null or currentUser = '') then
raise exception 'hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
end if;
return currentUser;
end; $$;
create or replace function currentUserId()
returns uuid
stable leakproof
language plpgsql as $$
declare
currentUser varchar(63);
currentUserId uuid;
begin
currentUser := currentUser();
currentUserId = (select uuid from RbacUser where name = currentUser);
if currentUserId is null then
raise exception 'hsadminng.currentUser defined as %, but does not exists', currentUser;
end if;
return currentUserId;
end; $$;
--//
-- ============================================================================
--changeset rbac-current-ASSUMED-ROLES:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Returns assumed role names as set in `hsadminng.assumedRoles`
or empty array, if not set.
*/
create or replace function assumedRoles()
returns varchar(63)[]
stable leakproof
language plpgsql as $$
declare
currentSubject varchar(63);
begin
begin
currentSubject := current_setting('hsadminng.assumedRoles');
exception
when others then
return array []::varchar[];
end;
if (currentSubject = '') then
return array []::varchar[];
end if;
return string_to_array(currentSubject, ';');
end; $$;
create or replace function pureIdentifier(rawIdentifier varchar)
returns varchar
returns null on null input
language plpgsql as $$
begin
return regexp_replace(rawIdentifier, '\W+', '');
end; $$;
create or replace function findObjectUuidByIdName(objectTable varchar, objectIdName varchar)
returns uuid
returns null on null input
language plpgsql as $$
declare
sql varchar;
uuid uuid;
begin
objectTable := pureIdentifier(objectTable);
objectIdName := pureIdentifier(objectIdName);
sql := format('select * from %sUuidByIdName(%L);', objectTable, objectIdName);
begin
raise notice 'sql: %', sql;
execute sql into uuid;
exception
when others then
raise exception 'function %UuidByIdName(...) not found, add identity view support for table %', objectTable, objectTable;
end;
return uuid;
end ; $$;
create or replace function findIdNameByObjectUuid(objectTable varchar, objectUuid uuid)
returns varchar
returns null on null input
language plpgsql as $$
declare
sql varchar;
idName varchar;
begin
objectTable := pureIdentifier(objectTable);
sql := format('select * from %sIdNameByUuid(%L::uuid);', objectTable, objectUuid);
begin
raise notice 'sql: %', sql;
execute sql into idName;
exception
when others then
raise exception 'function %IdNameByUuid(...) not found, add identity view support for table %', objectTable, objectTable;
end;
return idName;
end ; $$;
create or replace function currentSubjects()
returns varchar(63)[]
stable leakproof
language plpgsql as $$
declare
assumedRoles varchar(63)[];
begin
assumedRoles := assumedRoles();
if array_length(assumedRoles(), 1) > 0 then
return assumedRoles();
else
return array [currentUser()]::varchar(63)[];
end if;
end; $$;
create or replace function currentSubjectIds()
returns uuid[]
stable leakproof
language plpgsql as $$
declare
currentUserId uuid;
roleNames varchar(63)[];
roleName varchar(63);
objectTableToAssume varchar(63);
objectNameToAssume varchar(63);
objectUuidToAssume uuid;
roleTypeToAssume RbacRoleType;
roleIdsToAssume uuid[];
roleUuidToAssume uuid;
begin
currentUserId := currentUserId();
if currentUserId is null then
raise exception 'user % does not exist', currentUser();
end if;
roleNames := assumedRoles();
if cardinality(roleNames) = 0 then
return array [currentUserId];
end if;
raise notice 'assuming roles: %', roleNames;
foreach roleName in array roleNames
loop
roleName = overlay(roleName placing '#' from length(roleName) + 1 - strpos(reverse(roleName), '.'));
objectTableToAssume = split_part(roleName, '#', 1);
objectNameToAssume = split_part(roleName, '#', 2);
roleTypeToAssume = split_part(roleName, '#', 3);
objectUuidToAssume = findObjectUuidByIdName(objectTableToAssume, objectNameToAssume);
-- TODO: either the result needs to be cached at least per transaction or we need to get rid of SELCT in a loop
select uuid as roleuuidToAssume
from RbacRole r
where r.objectUuid = objectUuidToAssume
and r.roleType = roleTypeToAssume
into roleUuidToAssume;
if (not isGranted(currentUserId, roleUuidToAssume)) then
raise exception 'user % (%) has no permission to assume role % (%)', currentUser(), currentUserId, roleName, roleUuidToAssume;
end if;
roleIdsToAssume := roleIdsToAssume || roleUuidToAssume;
end loop;
return roleIdsToAssume;
end; $$;
--//

View File

@ -0,0 +1,91 @@
--liquibase formatted sql
-- ============================================================================
--changeset rbac-views-ROLE-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the role table with row-level limitation
based on the grants of the current user or assumed roles.
*/
drop view if exists rbacrole_rv;
create or replace view rbacrole_rv as
select DISTINCT r.*, o.objectTable,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
from rbacrole as r
join rbacobject as o on o.uuid=r.objectuuid
where isGranted(currentSubjectIds(), r.uuid);
grant all privileges on rbacrole_rv to restricted;
--//
-- ============================================================================
--changeset rbac-views-USER-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the users table with row-level limitation
based on the grants of the current user or assumed roles.
*/
drop view if exists RbacUser_rv;
create or replace view RbacUser_rv as
select u.*
from RbacUser as u
join RbacGrants as g on g.ascendantuuid = u.uuid
join rbacrole_rv as r on r.uuid = g.descendantuuid;
grant all privileges on RbacUser_rv to restricted;
--//
-- ============================================================================
--changeset rbac-views-OWN-GRANTED-PERMISSIONS-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to all permissions granted to the current user or
based on the grants of the current user or assumed roles.
*/
-- @formatter:off
drop view if exists RbacOwnGrantedPermissions_rv;
create or replace view RbacOwnGrantedPermissions_rv as
select r.uuid as roleuuid, p.uuid as permissionUuid,
(r.objecttable || '#' || r.objectidname || '.' || r.roletype) as roleName, p.op,
o.objecttable, r.objectidname, o.uuid as objectuuid
from rbacrole_rv r
join rbacgrants g on g.ascendantuuid = r.uuid
join rbacpermission p on p.uuid = g.descendantuuid
join rbacobject o on o.uuid = p.objectuuid;
grant all privileges on RbacOwnGrantedPermissions_rv to restricted;
-- @formatter:om
-- ============================================================================
--changeset rbac-views-GRANTED-PERMISSIONS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Returns all permissions granted to the given user,
which are also visible to the current user or assumed roles.
*/
create or replace function grantedPermissions(userName varchar)
returns table(roleUuid uuid, roleName text, permissionUuid uuid, op RbacOp, objectTable varchar, objectIdName varchar, objectUuid uuid)
returns null on null input
language plpgsql as $$
begin
-- @formatter:off
if cardinality(assumedRoles()) > 0 then
raise exception 'grantedPermissions(...) does not support assumed roles';
end if;
return query select
xp.roleUuid,
(xp.objecttable || '#' || xp.objectidname || '.' || xp.roletype) as roleName,
xp.permissionUuid, xp.op, xp.objecttable, xp.objectIdName, xp.objectuuid
from (select
r.uuid as roleUuid, r.roletype,
p.uuid as permissionUuid, p.op, o.objecttable,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName,
o.uuid as objectuuid
from queryPermissionsGrantedToSubjectId( findRbacUserId(userName)) p
join rbacgrants g on g.descendantuuid = p.uuid
join rbacobject o on o.uuid = p.objectuuid
join rbacrole r on r.uuid = g.ascendantuuid
where isGranted(currentUserId(), r.uuid)
) xp;
-- @formatter:on
end; $$;

View File

@ -168,6 +168,7 @@ $$;
*/
create or replace function packageIdNameByUuid(uuid uuid)
returns varchar
stable leakproof
language sql
strict as $$
select idName from package_iv iv where iv.uuid = packageIdNameByUuid.uuid;

View File

@ -9,6 +9,10 @@ databaseChangeLog:
file: db/changelog/2022-07-28-004-uuid-ossp-extension.sql
- include:
file: db/changelog/2022-07-28-005-rbac-base.sql
- include:
file: db/changelog/2022-07-28-006-rbac-current.sql
- include:
file: db/changelog/2022-07-28-007-rbac-views.sql
- include:
file: db/changelog/2022-07-28-020-rbac-role-builder.sql
- include: