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 = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-grants/{roleUuid}") .path("/api/rbac-grants/{roleUuid}")
.buildAndExpand(body.getRoleUuid()) .buildAndExpand(body.getGrantedRoleUuid())
.toUri(); .toUri();
return ResponseEntity.created(uri).build(); return ResponseEntity.created(uri).build();
} }

View File

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

View File

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

View File

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

View File

@ -353,11 +353,10 @@ $$;
*/ */
create table RbacGrants create table RbacGrants
( (
grantedByRoleUuid uuid references RbacRole (uuid) on delete cascade,
ascendantUuid uuid references RbacReference (uuid) on delete cascade, ascendantUuid uuid references RbacReference (uuid) on delete cascade,
descendantUuid 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) 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) primary key (ascendantUuid, descendantUuid)
); );
create index on RbacGrants (ascendantUuid); create index on RbacGrants (ascendantUuid);
@ -463,8 +462,8 @@ begin
perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission'); perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
insert insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) into RbacGrants (ascendantUuid, descendantUuid, assumed)
values (roleUuid, permissionIds[i], true, true, false) values (roleUuid, permissionIds[i], true)
on conflict do nothing; -- allow granting multiple times on conflict do nothing; -- allow granting multiple times
end loop; end loop;
end; end;
@ -476,13 +475,13 @@ begin
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole'); perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
perform assertReferenceType('subRoleId (descendant)', subRoleId, '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; raise exception '[400] Cyclic role grant detected between % and %', subRoleId, superRoleId;
end if; end if;
insert insert
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered) into RbacGrants (ascendantuuid, descendantUuid, assumed)
values (superRoleId, subRoleId, true, doAssume, false) values (superRoleId, subRoleId, doAssume)
on conflict do nothing; -- allow granting multiple times on conflict do nothing; -- allow granting multiple times
end; $$; end; $$;
@ -497,48 +496,6 @@ begin
end if; end if;
end; $$; 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:--// --changeset rbac-base-QUERY-ACCESSIBLE-OBJECT-UUIDS:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------

View File

@ -23,6 +23,7 @@ begin
if (currentUser is null or currentUser = '') then if (currentUser is null or currentUser = '') then
raise exception '[401] hsadminng.currentUser must be defined, please use "SET LOCAL ...;"'; raise exception '[401] hsadminng.currentUser must be defined, please use "SET LOCAL ...;"';
end if; end if;
raise debug 'currentUser: %', currentUser;
return currentUser; return currentUser;
end; $$; 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; drop view if exists rbacgrants_rv;
create or replace view rbacgrants_rv as 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 -- @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 ( 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 findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
from rbacgrants as g from rbacgrants as g
join rbacrole as r on r.uuid = g.descendantUuid join rbacrole as r on r.uuid = g.descendantUuid
join rbacobject o on o.uuid = r.objectuuid join rbacobject o on o.uuid = r.objectuuid
join rbacuser u on u.uuid = g.ascendantuuid join rbacuser u on u.uuid = g.ascendantuuid
where isGranted(currentSubjectIds(), r.uuid) 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 -- @formatter:on
order by roleIdName;
grant all privileges on rbacrole_rv to restricted; grant all privileges on rbacrole_rv to restricted;
--// --//
@ -67,15 +69,10 @@ create or replace function insertRbacGrant()
declare declare
newGrant RbacGrants_RV; newGrant RbacGrants_RV;
begin begin
if new.managed then call grantRoleToUser(assumedRoleUuid(), new.grantedRoleUuid, new.userUuid, new.assumed);
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));
select grv.* select grv.*
from RbacGrants_RV 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; into newGrant;
return newGrant; return newGrant;
end; $$; end; $$;
@ -88,6 +85,33 @@ create trigger insertRbacGrant_Trigger
on RbacGrants_rv on RbacGrants_rv
for each row for each row
execute function insertRbacGrant(); 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; ) xp;
-- @formatter:on -- @formatter:on
end; $$; end; $$;
--//

View File

@ -26,7 +26,7 @@ create or replace function withoutPermissions()
language plpgsql language plpgsql
strict as $$ strict as $$
begin begin
return row (array[]::uuid[]); return row (array []::uuid[]);
end; $$; end; $$;
--// --//
@ -167,7 +167,8 @@ create or replace function createRole(
permissions RbacPermissions, permissions RbacPermissions,
superRoles RbacSuperRoles, superRoles RbacSuperRoles,
subRoles RbacSubRoles = null, subRoles RbacSubRoles = null,
users RbacUsers = null users RbacUsers = null,
grantingRoleUuid uuid = null
) )
returns uuid returns uuid
called on null input called on null input
@ -200,7 +201,7 @@ begin
if users is not null then if users is not null then
foreach userUuid in array users.useruUids foreach userUuid in array users.useruUids
loop loop
call grantRoleToUser(roleUuid, userUuid); call grantRoleToUserUnchecked(grantingRoleUuid, roleUuid, userUuid);
end loop; end loop;
end if; end if;
@ -210,26 +211,47 @@ end; $$;
create or replace function createRole( create or replace function createRole(
roleDescriptor RbacRoleDescriptor, roleDescriptor RbacRoleDescriptor,
permissions RbacPermissions, permissions RbacPermissions,
users RbacUsers = null users RbacUsers = null,
grantingRoleUuid uuid = null
) )
returns uuid returns uuid
called on null input called on null input
language plpgsql as $$ language plpgsql as $$
begin begin
return createRole(roleDescriptor, permissions, null, null, users); return createRole(roleDescriptor, permissions, null, null, users, grantingRoleUuid);
end; $$; end; $$;
create or replace function createRole( create or replace function createRole(
roleDescriptor RbacRoleDescriptor, roleDescriptor RbacRoleDescriptor,
permissions RbacPermissions, permissions RbacPermissions,
subRoles RbacSubRoles, subRoles RbacSubRoles,
users RbacUsers = null users RbacUsers = null,
grantingRoleUuid uuid = null
) )
returns uuid returns uuid
called on null input called on null input
language plpgsql as $$ language plpgsql as $$
begin begin
return createRole(roleDescriptor, permissions, null, subRoles, users); return createRole(roleDescriptor, permissions, null, subRoles, users, grantingRoleUuid);
end; $$; 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 ; admins uuid ;
begin begin
admins = findRoleId(hostsharingAdmin()); admins = findRoleId(hostsharingAdmin());
call grantRoleToUser(admins, createRbacUser('mike@hostsharing.net')); call grantRoleToUserUnchecked(admins, admins, createRbacUser('mike@hostsharing.net'));
call grantRoleToUser(admins, createRbacUser('sven@hostsharing.net')); call grantRoleToUserUnchecked(admins, admins, createRbacUser('sven@hostsharing.net'));
end; end;
$$; $$;
--// --//

View File

@ -77,7 +77,8 @@ begin
customerAdmin(NEW), customerAdmin(NEW),
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']), grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']),
-- NO auto assume for customer owner to avoid exploding permissions for administrators -- 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 -- allow the customer owner role (thus administrators) to assume the customer admin role

View File

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

View File

@ -11,30 +11,32 @@ create or replace procedure createPackageTestData(
doCommitAfterEach boolean -- only for mass data creation outside of Liquibase doCommitAfterEach boolean -- only for mass data creation outside of Liquibase
) )
language plpgsql as $$ language plpgsql as $$
declare declare
cust customer; cust customer;
custAdminUser varchar;
custAdminRole varchar;
pacName varchar; pacName varchar;
currentTask varchar; currentTask varchar;
custAdmin varchar;
pac package; pac package;
begin begin
set hsadminng.currentUser to ''; set hsadminng.currentUser to '';
for cust in (select * from customer) for cust in (select * from customer)
loop loop
CONTINUE WHEN cust.reference < minCustomerReference; continue when cust.reference < minCustomerReference;
for t in 0..2 for t in 0..2
loop loop
pacName = cust.prefix || to_char(t, 'fm00'); pacName = cust.prefix || to_char(t, 'fm00');
currentTask = 'creating RBAC test package #' || pacName || ' for customer ' || cust.prefix || ' #' || currentTask = 'creating RBAC test package #' || pacName || ' for customer ' || cust.prefix || ' #' ||
cust.uuid; cust.uuid;
raise notice 'task: %', currentTask;
custAdmin = 'admin@' || cust.prefix || '.example.com'; custAdminUser = 'admin@' || cust.prefix || '.example.com';
set local hsadminng.currentUser to custAdmin; custAdminRole = 'customer#' || cust.prefix || '.admin';
set local hsadminng.assumedRoles = ''; execute format('set local hsadminng.currentUser to %L', custAdminUser);
set local hsadminng.currentTask to currentTask; 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 insert
into package (customerUuid, name, description) into package (customerUuid, name, description)
@ -42,8 +44,10 @@ create or replace procedure createPackageTestData(
returning * into pac; returning * into pac;
call grantRoleToUser( call grantRoleToUser(
getRoleId(customerAdmin(cust), 'fail'),
findRoleId(packageAdmin(pac)), findRoleId(packageAdmin(pac)),
createRbacUser(pacName || '@' || cust.prefix || '.example.com')); createRbacUser(pacName || '@' || cust.prefix || '.example.com'),
true);
end loop; end loop;
end loop; end loop;
@ -51,7 +55,7 @@ create or replace procedure createPackageTestData(
if doCommitAfterEach then if doCommitAfterEach then
commit; commit;
end if; end if;
end; end ;
$$; $$;
--// --//

View File

@ -12,7 +12,9 @@ databaseChangeLog:
- include: - include:
file: db/changelog/2022-07-28-006-rbac-current.sql file: db/changelog/2022-07-28-006-rbac-current.sql
- include: - 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: - include:
file: db/changelog/2022-07-28-020-rbac-role-builder.sql file: db/changelog/2022-07-28-020-rbac-role-builder.sql
- include: - include:

View File

@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.annotation.DirtiesContext;
import javax.transaction.Transactional; import javax.transaction.Transactional;
@ -11,6 +12,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = Context.class) @ComponentScan(basePackageClasses = Context.class)
@DirtiesContext
class ContextIntegrationTests { class ContextIntegrationTests {
@Autowired @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.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.persistence.PersistenceException; import javax.persistence.PersistenceException;
@ -19,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class }) @ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class CustomerRepositoryIntegrationTest { class CustomerRepositoryIntegrationTest {
@Autowired @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.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import javax.transaction.Transactional; import javax.transaction.Transactional;
@ -18,6 +19,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class }) @ComponentScan(basePackageClasses = { Context.class, CustomerRepository.class })
@DirtiesContext
class PackageRepositoryIntegrationTest { class PackageRepositoryIntegrationTest {
@Autowired @Autowired

View File

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

View File

@ -3,7 +3,9 @@ package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.Accepts; import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.context.Context; import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository; import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository; import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import net.hostsharing.test.JpaAttempt;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; 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.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import java.util.List; import java.util.List;
import java.util.UUID;
import static net.hostsharing.test.JpaAttempt.attempt; import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, RbacGrantRepository.class }) @ComponentScan(basePackageClasses = { RbacGrantRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext @DirtiesContext
@Accepts({ "GRT:S(Schema)" }) @Accepts({ "GRT:S(Schema)" })
class RbacGrantRepositoryIntegrationTest { class RbacGrantRepositoryIntegrationTest {
@ -39,6 +44,9 @@ class RbacGrantRepositoryIntegrationTest {
@Autowired @Autowired
EntityManager em; EntityManager em;
@Autowired
JpaAttempt jpaAttempt;
@Nested @Nested
class FindAllRbacGrants { class FindAllRbacGrants {
@ -54,7 +62,7 @@ class RbacGrantRepositoryIntegrationTest {
// then // then
exactlyTheseRbacGrantsAreReturned( exactlyTheseRbacGrantsAreReturned(
result, 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 @Test
@ -69,28 +77,26 @@ class RbacGrantRepositoryIntegrationTest {
// then // then
exactlyTheseRbacGrantsAreReturned( exactlyTheseRbacGrantsAreReturned(
result, result,
"grant( admin@aaa.example.com -> customer#aaa.admin: managed assumed empowered )", "{ grant assumed role customer#aaa.admin to user admin@aaa.example.com by role global#hostsharing.admin }",
"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 }",
"grant( aaa01@aaa.example.com -> package#aaa01.admin: managed assumed empowered )", "{ grant assumed role package#aaa01.admin to user aaa01@aaa.example.com by role customer#aaa.admin }",
"grant( aaa02@aaa.example.com -> package#aaa02.admin: managed assumed empowered )"); "{ grant assumed role package#aaa02.admin to user aaa02@aaa.example.com by role customer#aaa.admin }");
} }
@Test @Test
@Accepts({ "GRT:L(List)" }) @Accepts({ "GRT:L(List)" })
public void customerAdmin_withAssumedRole_cannotViewRbacGrants() { public void customerAdmin_withAssumedRole_canOnlyViewRbacGrantsVisibleByAssumedRole() {
// given: // given:
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
assumedRoles("package#aab00.admin"); assumedRoles("package#aaa00.admin");
// when // when
final var result = attempt( final var result = rbacGrantRepository.findAll();
em,
() -> rbacGrantRepository.findAll());
// then // then
result.assertExceptionWithRootCauseMessage( exactlyTheseRbacGrantsAreReturned(
JpaSystemException.class, result,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin"); "{ 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() { public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given // given
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
final var userUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com"); assumedRoles("customer#aaa.admin");
final var roleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid(); final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
// when // when
final var grant = RbacGrantEntity.builder() final var grant = RbacGrantEntity.builder()
.userUuid(userUuid).roleUuid(roleUuid) .granteeUserUuid(givenArbitraryUserUuid).grantedRoleUuid(givenOwnPackageRoleUuid)
.assumed(true).empowered(false) .assumed(true)
.build(); .build();
final var attempt = attempt(em, () -> final var attempt = attempt(em, () ->
rbacGrantRepository.save(grant) rbacGrantRepository.save(grant)
); );
// then // then
assertThat(attempt.wasSuccessful()).isTrue(); assertThat(attempt.caughtException()).isNull();
assertThat(rbacGrantRepository.findAll()) assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay) .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) { void currentUser(final String currentUser) {
@ -134,7 +188,7 @@ class RbacGrantRepositoryIntegrationTest {
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) { void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
assertThat(actualResult) 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) .extracting(RbacGrantEntity::toDisplay)
.containsExactlyInAnyOrder(expectedGrant); .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.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException; import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.Commit; import org.springframework.test.annotation.Commit;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -22,6 +23,7 @@ import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest @DataJpaTest
@ComponentScan(basePackageClasses = { RbacUserRepository.class, Context.class, JpaAttempt.class }) @ComponentScan(basePackageClasses = { RbacUserRepository.class, Context.class, JpaAttempt.class })
@DirtiesContext
class RbacUserRepositoryIntegrationTest { class RbacUserRepositoryIntegrationTest {
@Autowired @Autowired
@ -58,7 +60,7 @@ class RbacUserRepositoryIntegrationTest {
@Test @Test
@Commit @Commit
@Transactional(propagation = Propagation.NOT_SUPPORTED) @Transactional(propagation = Propagation.NEVER)
void anyoneCanCreateTheirOwnUser_committed() { void anyoneCanCreateTheirOwnUser_committed() {
// given: // given:

View File

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