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
(
grantedByRoleUuid uuid references RbacRole (uuid) on delete cascade,
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
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
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.*, u.name as userName, o.objecttable, r.objectuuid, r.roletype,
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
) as g
join RbacRole as r on r.uuid = grantedByRoleUuid
join RbacObject as o on o.uuid = r.objectUuid
order by grantedRoleIdName;
-- @formatter:on
order by roleIdName;
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

@ -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

@ -13,28 +13,30 @@ create or replace procedure createPackageTestData(
language plpgsql as $$
declare
cust customer;
custAdminUser varchar;
custAdminRole varchar;
pacName varchar;
currentTask varchar;
custAdmin varchar;
pac package;
begin
set hsadminng.currentUser to '';
for cust in (select * from customer)
loop
CONTINUE WHEN cust.reference < minCustomerReference;
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;
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)
@ -42,8 +44,10 @@ create or replace procedure createPackageTestData(
returning * into pac;
call grantRoleToUser(
getRoleId(customerAdmin(cust), 'fail'),
findRoleId(packageAdmin(pac)),
createRbacUser(pacName || '@' || cust.prefix || '.example.com'));
createRbacUser(pacName || '@' || cust.prefix || '.example.com'),
true);
end loop;
end loop;

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)
.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(givenAlienPackageAdminRole).getUuid().toString())
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString(),
createRBacUser(givenNewUserName).getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/rbac-grants")
.then().assertThat()
.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, () -> {
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;
}