diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java new file mode 100644 index 00000000..b2ca0ea3 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserController.java @@ -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 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 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); + } +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java new file mode 100644 index 00000000..4b930e3d --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserEntity.java @@ -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; +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java new file mode 100644 index 00000000..ba251885 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserPermission.java @@ -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(); + +} diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java new file mode 100644 index 00000000..502e0fb8 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacuser/RbacUserRepository.java @@ -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 { + + @Query("SELECT u FROM RbacUserEntity u WHERE :userName is null or u.name like concat(:userName, '%')") + List findByOptionalNameLike(final String userName); + + @Query(value = "SELECT * FROM grantedPermissions(:userName)", nativeQuery = true) + Iterable findPermissionsOfUser(@Param("userName") String userName); +} diff --git a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql index 6b5819fd..c21c8aa9 100644 --- a/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql +++ b/src/main/resources/db/changelog/2022-07-28-005-rbac-base.sql @@ -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; ---// diff --git a/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql b/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql new file mode 100644 index 00000000..6ef9a61c --- /dev/null +++ b/src/main/resources/db/changelog/2022-07-28-006-rbac-current.sql @@ -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; $$; +--// + diff --git a/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql b/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql new file mode 100644 index 00000000..f54b1bed --- /dev/null +++ b/src/main/resources/db/changelog/2022-07-28-007-rbac-views.sql @@ -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; $$; diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql index d7588ec4..6e2c0eeb 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql @@ -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; diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index ea0e640f..b86d0f2e 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -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: