implements user granting roles to other users

This commit is contained in:
Michael Hoennig 2022-08-16 10:46:41 +02:00
parent 7869d07d30
commit c8e835f880
21 changed files with 425 additions and 227 deletions

View File

@ -56,7 +56,7 @@ public class RbacGrantController implements RbacgrantsApi {
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-grants/{roleUuid}")
.buildAndExpand(body.getRoleUuid())
.buildAndExpand(body.getGrantedRoleUuid())
.toUri();
return ResponseEntity.created(uri).build();
}

View File

@ -18,24 +18,27 @@ import java.util.UUID;
@NoArgsConstructor
@AllArgsConstructor
public class RbacGrantEntity {
@Column(name = "grantedbyroleidname", updatable = false, insertable = false)
private String grantedByRoleIdName;
@Column(name = "grantedroleidname", updatable = false, insertable = false)
private String grantedRoleIdName;
@Column(name = "username", updatable = false, insertable = false)
private String userName;
private String granteeUserName;
@Column(name = "roleidname", updatable = false, insertable = false)
private String roleIdName;
private boolean managed;
private boolean assumed;
private boolean empowered;
@Column(name = "grantedbyroleuuid", updatable = false, insertable = false)
private UUID grantedByRoleUuid;
@Id
@Column(name = "grantedroleuuid")
private UUID grantedRoleUuid;
@Id
@Column(name = "useruuid")
private UUID userUuid;
@Id
@Column(name = "roleuuid")
private UUID roleUuid;
private UUID granteeUserUuid;
@Column(name = "objecttable", updatable = false, insertable = false)
private String objectTable;
@ -46,15 +49,12 @@ public class RbacGrantEntity {
@Column(name = "objectidname", updatable = false, insertable = false)
private String objectIdName;
@Column(name = "roletype", updatable = false, insertable = false)
@Column(name = "grantedroletype", updatable = false, insertable = false)
@Enumerated(EnumType.STRING)
private RbacRoleType roleType;
private RbacRoleType grantedRoleType;
public String toDisplay() {
return "grant( " + userName + " -> " + roleIdName + ": " +
(managed ? "managed " : "") +
(assumed ? "assumed " : "") +
(empowered ? "empowered " : "") +
")";
return "{ grant " + (assumed ? "assumed " : "") +
"role " + grantedRoleIdName + " to user " + granteeUserName + " by role " + grantedByRoleIdName + " }";
}
}

View File

@ -12,6 +12,6 @@ import java.util.UUID;
@NoArgsConstructor
public class RbacGrantId implements Serializable {
private UUID userUuid;
private UUID roleUuid;
private UUID granteeUserUuid;
private UUID grantedRoleUuid;
}

View File

@ -6,16 +6,14 @@ components:
RbacGrant:
type: object
properties:
userUuid:
type: string
format: uuid
roleUuid:
type: string
format: uuid
assumed:
type: boolean
empowered:
type: boolean
grantedRoleUuid:
type: string
format: uuid
granteeUserUuid:
type: string
format: uuid
required:
- userUuid
- roleUuid
- grantedRoleUuid
- granteeUserUuid

View File

@ -353,11 +353,10 @@ $$;
*/
create table RbacGrants
(
ascendantUuid uuid references RbacReference (uuid) on delete cascade,
descendantUuid uuid references RbacReference (uuid) on delete cascade,
managed boolean not null default false, -- created by system (true) vs. user (false)
assumed boolean not null default true, -- auto assumed (true) vs. needs assumeRoles (false)
empowered boolean not null default false, -- true: allows grant+revoke for descendant role
grantedByRoleUuid uuid references RbacRole (uuid) on delete cascade,
ascendantUuid uuid references RbacReference (uuid) on delete cascade,
descendantUuid uuid references RbacReference (uuid) on delete cascade,
assumed boolean not null default true, -- auto assumed (true) vs. needs assumeRoles (false)
primary key (ascendantUuid, descendantUuid)
);
create index on RbacGrants (ascendantUuid);
@ -463,8 +462,8 @@ begin
perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (roleUuid, permissionIds[i], true, true, false)
into RbacGrants (ascendantUuid, descendantUuid, assumed)
values (roleUuid, permissionIds[i], true)
on conflict do nothing; -- allow granting multiple times
end loop;
end;
@ -476,13 +475,13 @@ begin
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
perform assertReferenceType('subRoleId (descendant)', subRoleId, 'RbacRole');
if (isGranted(subRoleId, superRoleId)) then
if isGranted(subRoleId, superRoleId) then
raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
end if;
insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (superRoleId, subRoleId, true, doAssume, false)
into RbacGrants (ascendantuuid, descendantUuid, assumed)
values (superRoleId, subRoleId, doAssume)
on conflict do nothing; -- allow granting multiple times
end; $$;
@ -497,48 +496,6 @@ begin
end if;
end; $$;
create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid)
language plpgsql as $$
begin
perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole');
perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser');
insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (userUuid, roleUuid, true, true, true);
-- TODO: What should happen on mupltiple grants? What if options are not the same?
-- on conflict do nothing; -- allow granting multiple times
end; $$;
/*
Attributes of a grant assignment.
*/
create type RbacGrantOptions as
(
managed boolean, -- created by system (true) vs. user (false)
assumed boolean, -- auto assumed (true) vs. needs assumeRoles (false)
empowered boolean -- true: allows grant+revoke for descendant role
);
create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid, grantOptions RbacGrantOptions)
language plpgsql as $$
begin
perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole');
perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser');
if not isGranted(currentSubjectIds(), roleUuid) then
raise exception '[403] Access to role uuid % forbidden for %', roleUuid, currentSubjects();
end if;
insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (userUuid, roleUuid, grantOptions.managed, grantOptions.assumed, grantOptions.empowered);
-- TODO: What should happen on mupltiple grants? What if options are not the same?
-- Most powerful or latest grant wins? What about managed?
-- on conflict do nothing; -- allow granting multiple times
end; $$;
--//
-- ============================================================================
--changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------

View File

@ -23,6 +23,7 @@ begin
if (currentUser is null or currentUser = '') then
raise exception '[401] hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
end if;
raise debug 'currentUser: %', currentUser;
return currentUser;
end; $$;

View File

@ -0,0 +1,101 @@
--liquibase formatted sql
-- ============================================================================
--changeset rbac-user-grant-GRANT-ROLE-TO-USER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace function assumedRoleUuid()
returns uuid
stable leakproof
language plpgsql as $$
declare
currentSubjectUuids uuid[];
begin
-- exactly one role must be assumed, not none not more than one
if cardinality(assumedRoles()) <> 1 then
raise exception '[400] Granting roles to user is only possible if exactly one role is assumed, given: %', assumedRoles();
end if;
currentSubjectUuids := currentSubjectIds();
return currentSubjectUuids[1];
end; $$;
create or replace procedure grantRoleToUserUnchecked(grantedByRoleUuid uuid, roleUuid uuid, userUuid uuid, doAssume boolean = true)
language plpgsql as $$
begin
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole');
perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser');
insert
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
values (grantedByRoleUuid, userUuid, roleUuid, doAssume);
-- TODO: What should happen on mupltiple grants? What if options are not the same?
-- Most powerful or latest grant wins? What about managed?
-- on conflict do nothing; -- allow granting multiple times
end; $$;
create or replace procedure grantRoleToUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid, doAssume boolean = true)
language plpgsql as $$
begin
perform assertReferenceType('grantingRoleUuid', grantedByRoleUuid, 'RbacRole');
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
if NOT isGranted(currentSubjectIds(), grantedByRoleUuid) then
raise exception '[403] Access to granted-by-role % forbidden for %', grantedByRoleUuid, currentSubjects();
end if;
if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then
raise exception '[403] Access to granted role % forbidden for %', grantedRoleUuid, currentSubjects();
end if;
insert
into RbacGrants (grantedByRoleUuid, ascendantUuid, descendantUuid, assumed)
values (grantedByRoleUuid, userUuid, grantedRoleUuid, doAssume);
-- TODO: What should happen on mupltiple grants? What if options are not the same?
-- Most powerful or latest grant wins? What about managed?
-- on conflict do nothing; -- allow granting multiple times
end; $$;
--//
-- ============================================================================
--changeset rbac-user-grant-REVOKE-ROLE-FROM-USER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
create or replace procedure checkRevokeRoleFromUserPreconditions(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid)
language plpgsql as $$
begin
perform assertReferenceType('grantedByRoleUuid', grantedByRoleUuid, 'RbacRole');
perform assertReferenceType('grantedRoleUuid (descendant)', grantedRoleUuid, 'RbacRole');
perform assertReferenceType('userUuid (ascendant)', userUuid, 'RbacUser');
if NOT isGranted(currentSubjectIds(), grantedByRoleUuid) then
raise exception '[403] Revoking role created by % is forbidden for %.', grantedByRoleUuid, currentSubjects();
end if;
if NOT isGranted(grantedByRoleUuid, grantedRoleUuid) then
raise exception '[403] Revoking role % is forbidden for %.', grantedRoleUuid, currentSubjects();
end if;
if NOT isGranted(currentSubjectIds(), grantedByRoleUuid) then
raise exception '[403] Revoking role granted by % is forbidden for %.', grantedByRoleUuid, currentSubjects();
end if;
if NOT isGranted(userUuid, grantedRoleUuid) then
raise exception '[404] No such grant found granted by % for user % to role %.', grantedByRoleUuid, userUuid, grantedRoleUuid;
end if;
end; $$;
create or replace procedure revokeRoleFromUser(grantedByRoleUuid uuid, grantedRoleUuid uuid, userUuid uuid)
language plpgsql as $$
begin
call checkRevokeRoleFromUserPreconditions(grantedByRoleUuid, grantedRoleUuid, userUuid);
raise INFO 'delete from RbacGrants where ascendantUuid = % and descendantUuid = %', userUuid, grantedRoleUuid;
delete from RbacGrants as g
where g.ascendantUuid = userUuid and g.descendantUuid = grantedRoleUuid
and g.grantedByRoleUuid = revokeRoleFromUser.grantedByRoleUuid;
end; $$;
--/

View File

@ -33,23 +33,25 @@ grant all privileges on rbacrole_rv to restricted;
*/
drop view if exists rbacgrants_rv;
create or replace view rbacgrants_rv as
select userName, objectTable||'#'||objectIdName||'.'||roletype as roleIdName,
managed, assumed, empowered,
ascendantUuid as userUuid,
descendantUuid as roleUuid,
objectTable, objectUuid, objectIdName, roleType
-- @formatter:off
from (
select g.*, u.name as userName, o.objecttable, r.objectuuid, r.roletype,
-- @formatter:off
select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || '.' || r.roletype as grantedByRoleIdName,
g.objectTable || '#' || g.objectIdName || '.' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
from (
select g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
u.name as userName, o.objecttable, r.objectuuid, r.roletype,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
from rbacgrants as g
join rbacrole as r on r.uuid = g.descendantUuid
join rbacobject o on o.uuid = r.objectuuid
join rbacuser u on u.uuid = g.ascendantuuid
where isGranted(currentSubjectIds(), r.uuid)
) as unordered
-- @formatter:on
order by roleIdName;
) as g
join RbacRole as r on r.uuid = grantedByRoleUuid
join RbacObject as o on o.uuid = r.objectUuid
order by grantedRoleIdName;
-- @formatter:on
grant all privileges on rbacrole_rv to restricted;
--//
@ -67,15 +69,10 @@ create or replace function insertRbacGrant()
declare
newGrant RbacGrants_RV;
begin
if new.managed then
raise exception '[400] Managed grants cannot be inserted via RBacGrants_RV.';
end if;
call grantRoleToUser(new.roleUuid, new.userUuid,
ROW(false, new.assumed, new.empowered));
call grantRoleToUser(assumedRoleUuid(), new.grantedRoleUuid, new.userUuid, new.assumed);
select grv.*
from RbacGrants_RV grv
where grv.userUuid=new.userUuid and grv.roleUuid=new.roleUuid
where grv.userUuid=new.userUuid and grv.grantedRoleUuid=new.grantedRoleUuid
into newGrant;
return newGrant;
end; $$;
@ -88,6 +85,33 @@ create trigger insertRbacGrant_Trigger
on RbacGrants_rv
for each row
execute function insertRbacGrant();
--/
-- ============================================================================
--changeset rbac-views-GRANTS-RV-DELETE-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Instead of delete trigger function for RbacGrants_RV.
*/
create or replace function deleteRbacGrant()
returns trigger
language plpgsql as $$
begin
call revokeRoleFromUser(assumedRoleUuid(), old.grantedRoleUuid, old.userUuid);
return null;
end; $$;
/*
Creates an instead of delete trigger for the RbacGrants_rv view.
*/
create trigger deleteRbacGrant_Trigger
instead of delete
on RbacGrants_rv
for each row
execute function deleteRbacGrant();
--/
-- ============================================================================
@ -220,3 +244,4 @@ begin
) xp;
-- @formatter:on
end; $$;
--//

View File

@ -26,7 +26,7 @@ create or replace function withoutPermissions()
language plpgsql
strict as $$
begin
return row (array[]::uuid[]);
return row (array []::uuid[]);
end; $$;
--//
@ -167,7 +167,8 @@ create or replace function createRole(
permissions RbacPermissions,
superRoles RbacSuperRoles,
subRoles RbacSubRoles = null,
users RbacUsers = null
users RbacUsers = null,
grantingRoleUuid uuid = null
)
returns uuid
called on null input
@ -200,7 +201,7 @@ begin
if users is not null then
foreach userUuid in array users.useruUids
loop
call grantRoleToUser(roleUuid, userUuid);
call grantRoleToUserUnchecked(grantingRoleUuid, roleUuid, userUuid);
end loop;
end if;
@ -210,26 +211,47 @@ end; $$;
create or replace function createRole(
roleDescriptor RbacRoleDescriptor,
permissions RbacPermissions,
users RbacUsers = null
users RbacUsers = null,
grantingRoleUuid uuid = null
)
returns uuid
called on null input
language plpgsql as $$
begin
return createRole(roleDescriptor, permissions, null, null, users);
return createRole(roleDescriptor, permissions, null, null, users, grantingRoleUuid);
end; $$;
create or replace function createRole(
roleDescriptor RbacRoleDescriptor,
permissions RbacPermissions,
subRoles RbacSubRoles,
users RbacUsers = null
users RbacUsers = null,
grantingRoleUuid uuid = null
)
returns uuid
called on null input
language plpgsql as $$
begin
return createRole(roleDescriptor, permissions, null, subRoles, users);
return createRole(roleDescriptor, permissions, null, subRoles, users, grantingRoleUuid);
end; $$;
--//
-- =================================================================
-- CREATE ROLE
--changeset rbac-role-builder-GRANTED-BY-ROLE:1 endDelimiter:--//
-- -----------------------------------------------------------------
/*
Used in role-builder-DSL to convert a role descriptor to it's uuid
for use as `grantedByRoleUuid`.
*/
create or replace function grantedByRole(roleDescriptor RbacRoleDescriptor)
returns uuid
strict leakproof
language plpgsql as $$
begin
return getRoleId(roledescriptor, 'fail');
end; $$;
--//

View File

@ -104,8 +104,8 @@ do language plpgsql $$
admins uuid ;
begin
admins = findRoleId(hostsharingAdmin());
call grantRoleToUser(admins, createRbacUser('mike@hostsharing.net'));
call grantRoleToUser(admins, createRbacUser('sven@hostsharing.net'));
call grantRoleToUserUnchecked(admins, admins, createRbacUser('mike@hostsharing.net'));
call grantRoleToUserUnchecked(admins, admins, createRbacUser('sven@hostsharing.net'));
end;
$$;
--//

View File

@ -77,7 +77,8 @@ begin
customerAdmin(NEW),
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']),
-- NO auto assume for customer owner to avoid exploding permissions for administrators
withUser(NEW.adminUserName, 'create') -- implicitly ignored if null
withUser(NEW.adminUserName, 'create'), -- implicitly ignored if null
grantedByRole(hostsharingAdmin())
);
-- allow the customer owner role (thus administrators) to assume the customer admin role

View File

@ -37,8 +37,8 @@ begin
loop
currentTask = 'creating RBAC test customer #' || t;
set local hsadminng.currentUser to 'mike@hostsharing.net';
set local hsadminng.assumedRoles = '';
set local hsadminng.currentTask to currentTask;
set local hsadminng.assumedRoles to 'global#hostsharing.admin';
execute format('set local hsadminng.currentTask to %L', currentTask);
-- When a new customer is created,
custReference = testCustomerReference(t);

View File

@ -7,51 +7,55 @@
Creates test data for the package main table.
*/
create or replace procedure createPackageTestData(
minCustomerReference integer, -- skip customers with reference below this
doCommitAfterEach boolean -- only for mass data creation outside of Liquibase
minCustomerReference integer, -- skip customers with reference below this
doCommitAfterEach boolean -- only for mass data creation outside of Liquibase
)
language plpgsql as $$
declare
cust customer;
pacName varchar;
currentTask varchar;
custAdmin varchar;
pac package;
begin
set hsadminng.currentUser to '';
declare
cust customer;
custAdminUser varchar;
custAdminRole varchar;
pacName varchar;
currentTask varchar;
pac package;
begin
set hsadminng.currentUser to '';
for cust in (select * from customer)
loop
CONTINUE WHEN cust.reference < minCustomerReference;
for cust in (select * from customer)
loop
continue when cust.reference < minCustomerReference;
for t in 0..2
loop
pacName = cust.prefix || to_char(t, 'fm00');
currentTask = 'creating RBAC test package #' || pacName || ' for customer ' || cust.prefix || ' #' ||
cust.uuid;
raise notice 'task: %', currentTask;
for t in 0..2
loop
pacName = cust.prefix || to_char(t, 'fm00');
currentTask = 'creating RBAC test package #' || pacName || ' for customer ' || cust.prefix || ' #' ||
cust.uuid;
custAdmin = 'admin@' || cust.prefix || '.example.com';
set local hsadminng.currentUser to custAdmin;
set local hsadminng.assumedRoles = '';
set local hsadminng.currentTask to currentTask;
custAdminUser = 'admin@' || cust.prefix || '.example.com';
custAdminRole = 'customer#' || cust.prefix || '.admin';
execute format('set local hsadminng.currentUser to %L', custAdminUser);
execute format('set local hsadminng.assumedRoles to %L', custAdminRole);
execute format('set local hsadminng.currentTask to %L', currentTask);
raise notice 'task: % by % as %', currentTask, custAdminUser, custAdminRole;
insert
into package (customerUuid, name, description)
values (cust.uuid, pacName, 'Here can add your own description of package ' || pacName || '.')
returning * into pac;
insert
into package (customerUuid, name, description)
values (cust.uuid, pacName, 'Here can add your own description of package ' || pacName || '.')
returning * into pac;
call grantRoleToUser(
findRoleId(packageAdmin(pac)),
createRbacUser(pacName || '@' || cust.prefix || '.example.com'));
call grantRoleToUser(
getRoleId(customerAdmin(cust), 'fail'),
findRoleId(packageAdmin(pac)),
createRbacUser(pacName || '@' || cust.prefix || '.example.com'),
true);
end loop;
end loop;
end loop;
end loop;
if doCommitAfterEach then
commit;
end if;
end;
if doCommitAfterEach then
commit;
end if;
end ;
$$;
--//

View File

@ -12,7 +12,9 @@ databaseChangeLog:
- include:
file: db/changelog/2022-07-28-006-rbac-current.sql
- include:
file: db/changelog/2022-07-28-007-rbac-views.sql
file: db/changelog/2022-07-28-007-rbac-user-grant.sql
- include:
file: db/changelog/2022-07-28-008-rbac-views.sql
- include:
file: db/changelog/2022-07-28-020-rbac-role-builder.sql
- include:

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
import javax.transaction.Transactional;
@ -11,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = Context.class)
@DirtiesContext
class ContextIntegrationTests {
@Autowired

View File

@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;
@ -19,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class CustomerRepositoryIntegrationTest {
@Autowired

View File

@ -8,6 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import javax.transaction.Transactional;
@ -18,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class PackageRepositoryIntegrationTest {
@Autowired

View File

@ -15,19 +15,22 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.CoreMatchers.containsString;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Accepts({ "ROL:S(Schema)" })
@Transactional(propagation = Propagation.NEVER)
class RbacGrantControllerAcceptanceTest {
@LocalServerPort
@ -57,23 +60,25 @@ class RbacGrantControllerAcceptanceTest {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
final String givenAssumedRole = "package#aaa00.admin";
final var givenOwnPackageAdminRole = "package#aaa00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.header("current-user", givenCurrentUserPackageAdmin)
.header("assumed-roles", givenAssumedRole)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
"grantedRoleUuid": "%s",
"granteeUserUuid": "%s"
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString())
findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString(),
createRBacUser(givenNewUserName).getUuid().toString())
)
.port(port)
.when()
@ -83,9 +88,11 @@ class RbacGrantControllerAcceptanceTest {
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( " + givenNewUserName + " -> " + givenOwnPackageAdminRole + ": assumed )");
.contains("{ grant assumed role " + givenOwnPackageAdminRole +
" to user " + givenNewUserName +
" by role " + givenAssumedRole + " }");
}
@Test
@ -94,35 +101,38 @@ class RbacGrantControllerAcceptanceTest {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final String givenCurrentUserPackageAdmin = "aaa00@aaa.example.com";
final String givenAssumedRole = "package#aaa00.admin";
final var givenAlienPackageAdminRole = "package#aab00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString())
)
.port(port)
.header("current-user", givenCurrentUserPackageAdmin)
.header("assumed-roles", givenAssumedRole)
.contentType(ContentType.JSON)
.body("""
{
"assumed": true,
"grantedRoleUuid": "%s",
"granteeUserUuid": "%s"
}
""".formatted(
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString(),
createRBacUser(givenNewUserName).getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/rbac-grants")
.post("http://localhost/api/rbac-grants")
.then().assertThat()
.statusCode(403);
.body("message", containsString("Access to granted role"))
.body("message", containsString("forbidden for {package#aaa00.admin}"))
.statusCode(403);
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
.extracting(RbacGrantEntity::getUserName)
assertThat(findAllGrantsOfUser(givenCurrentUserPackageAdmin))
.extracting(RbacGrantEntity::getGranteeUserName)
.doesNotContain(givenNewUserName);
}
@ -134,9 +144,9 @@ class RbacGrantControllerAcceptanceTest {
}
RbacUserEntity createRBacUser(final String userName) {
return jpaAttempt.transacted(() -> {
return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName));
}).returnedValue();
return jpaAttempt.transacted(() ->
rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName))
).returnedValue();
}
RbacRoleEntity findRbacRoleByName(final String roleName) {
@ -145,5 +155,4 @@ class RbacGrantControllerAcceptanceTest {
return rbacRoleRepository.findByRoleName(roleName);
}).returnedValue();
}
}

View File

@ -3,7 +3,9 @@ package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@ -11,15 +13,18 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, RbacGrantRepository.class })
@ComponentScan(basePackageClasses = { RbacGrantRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
@Accepts({ "GRT:S(Schema)" })
class RbacGrantRepositoryIntegrationTest {
@ -39,6 +44,9 @@ class RbacGrantRepositoryIntegrationTest {
@Autowired
EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Nested
class FindAllRbacGrants {
@ -54,7 +62,7 @@ class RbacGrantRepositoryIntegrationTest {
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )");
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
}
@Test
@ -69,28 +77,26 @@ class RbacGrantRepositoryIntegrationTest {
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( admin@aaa.example.com -> customer#aaa.admin: managed assumed empowered )",
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )",
"grant( aaa01@aaa.example.com -> package#aaa01.admin: managed assumed empowered )",
"grant( aaa02@aaa.example.com -> package#aaa02.admin: managed assumed empowered )");
"{ grant assumed role customer#aaa.admin to user admin@aaa.example.com by role global#hostsharing.admin }",
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa01.admin to user aaa01@aaa.example.com by role customer#aaa.admin }",
"{ grant assumed role package#aaa02.admin to user aaa02@aaa.example.com by role customer#aaa.admin }");
}
@Test
@Accepts({ "GRT:L(List)" })
public void customerAdmin_withAssumedRole_cannotViewRbacGrants() {
public void customerAdmin_withAssumedRole_canOnlyViewRbacGrantsVisibleByAssumedRole() {
// given:
currentUser("admin@aaa.example.com");
assumedRoles("package#aab00.admin");
assumedRoles("package#aaa00.admin");
// when
final var result = attempt(
em,
() -> rbacGrantRepository.findAll());
final var result = rbacGrantRepository.findAll();
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
exactlyTheseRbacGrantsAreReturned(
result,
"{ grant assumed role package#aaa00.admin to user aaa00@aaa.example.com by role customer#aaa.admin }");
}
}
@ -102,24 +108,72 @@ class RbacGrantRepositoryIntegrationTest {
public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given
currentUser("admin@aaa.example.com");
final var userUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var roleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
assumedRoles("customer#aaa.admin");
final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
// when
final var grant = RbacGrantEntity.builder()
.userUuid(userUuid).roleUuid(roleUuid)
.assumed(true).empowered(false)
.granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true)
.build();
final var attempt = attempt(em, () ->
rbacGrantRepository.save(grant)
);
// then
assertThat(attempt.wasSuccessful()).isTrue();
assertThat(attempt.caughtException()).isNull();
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( aac00@aac.example.com -> package#aaa00.admin: assumed )");
.contains("{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
}
@Test
@Accepts({ "GRT:C(Create)" })
@Transactional(propagation = Propagation.NEVER)
public void packageAdmin_canNotGrantPackageOwnerRole() {
// given
record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) {}
final var given = jpaAttempt.transacted(() -> {
// to find the uuids of we need to have access rights to these
currentUser("admin@aaa.example.com");
return new Given(
createNewUser(), // eigene Transaktion?
rbacRoleRepository.findByRoleName("package#aaa00.owner").getUuid()
);
}).returnedValue();
// when
final var attempt = jpaAttempt.transacted(() -> {
// now we try to use these uuids as a less privileged user
currentUser("aaa00@aaa.example.com");
assumedRoles("package#aaa00.admin");
final var grant = RbacGrantEntity.builder()
.granteeUserUuid(given.arbitraryUser.getUuid())
.grantedRoleUuid(given.packageOwnerRoleUuid)
.assumed(true)
.build();
rbacGrantRepository.save(grant);
});
// then
attempt.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"ERROR: [403] Access to granted role " + given.packageOwnerRoleUuid
+ " forbidden for {package#aaa00.admin}");
jpaAttempt.transacted(() -> {
currentUser(given.arbitraryUser.getName());
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.hasSize(0);
// "{ grant assumed role package#aaa00.admin to user aac00@aac.example.com by role customer#aaa.admin }");
});
}
}
private RbacUserEntity createNewUser() {
return rbacUserRepository.create(
new RbacUserEntity(null, "test-user-" + System.currentTimeMillis() + "@example.com"));
}
void currentUser(final String currentUser) {
@ -134,7 +188,7 @@ class RbacGrantRepositoryIntegrationTest {
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
assertThat(actualResult)
.filteredOn(g -> !g.getUserName().startsWith("test-user-")) // ignore test-users created by other tests
.filteredOn(g -> !g.getGranteeUserName().startsWith("test-user-")) // ignore test-users created by other tests
.extracting(RbacGrantEntity::toDisplay)
.containsExactlyInAnyOrder(expectedGrant);
}

View File

@ -10,6 +10,7 @@ import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@ -22,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { RbacUserRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
class RbacUserRepositoryIntegrationTest {
@Autowired
@ -58,7 +60,7 @@ class RbacUserRepositoryIntegrationTest {
@Test
@Commit
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@Transactional(propagation = Propagation.NEVER)
void anyoneCanCreateTheirOwnUser_committed() {
// given:

View File

@ -4,8 +4,7 @@ import junit.framework.AssertionFailedError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.NestedExceptionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import java.util.Optional;
@ -32,20 +31,16 @@ import static org.assertj.core.api.Assertions.assertThat;
public class JpaAttempt {
@Autowired
private final EntityManager em;
public JpaAttempt(final EntityManager em) {
this.em = em;
}
private TransactionTemplate transactionTemplate;
public static <T> JpaResult<T> attempt(final EntityManager em, final Supplier<T> code) {
try {
final var result = new JpaResult<T>(code.get(), null);
final var result = JpaResult.forValue(code.get());
em.flush();
em.clear();
return result;
} catch (RuntimeException exc) {
return new JpaResult<T>(null, exc);
} catch (final RuntimeException exc) {
return JpaResult.forException(exc);
}
}
@ -56,29 +51,50 @@ public class JpaAttempt {
});
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public <T> JpaResult<T> transacted(final Supplier<T> code) {
return attempt(em, code);
try {
return JpaResult.forValue(
transactionTemplate.execute(transactionStatus -> code.get()));
} catch (final RuntimeException exc) {
return JpaResult.forException(exc);
}
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transacted(final Runnable code) {
attempt(em, () -> {
code.run();
return null;
});
public JpaResult<Void> transacted(final Runnable code) {
try {
transactionTemplate.execute(transactionStatus -> {
code.run();
return null;
});
return JpaResult.forVoidValue();
} catch (final RuntimeException exc) {
return new JpaResult<>(null, exc);
}
}
public static class JpaResult<T> {
final T result;
final RuntimeException exception;
private final T result;
private final RuntimeException exception;
public JpaResult(final T result, final RuntimeException exception) {
private JpaResult(final T result, final RuntimeException exception) {
this.result = result;
this.exception = exception;
}
static JpaResult<Void> forVoidValue() {
return new JpaResult<>(null, null);
}
public static <T> JpaResult<T> forValue(final T value) {
return new JpaResult<>(value, null);
}
public static <T> JpaResult<T> forException(final RuntimeException exception) {
return new JpaResult<>(null, exception);
}
public boolean wasSuccessful() {
return exception == null;
}