implements user granting roles to other users
This commit is contained in:
parent
7869d07d30
commit
c8e835f880
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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 " : "") +
|
|
||||||
")";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -353,11 +353,10 @@ $$;
|
|||||||
*/
|
*/
|
||||||
create table RbacGrants
|
create table RbacGrants
|
||||||
(
|
(
|
||||||
ascendantUuid uuid references RbacReference (uuid) on delete cascade,
|
grantedByRoleUuid uuid references RbacRole (uuid) on delete cascade,
|
||||||
descendantUuid uuid references RbacReference (uuid) on delete cascade,
|
ascendantUuid uuid references RbacReference (uuid) on delete cascade,
|
||||||
managed boolean not null default false, -- created by system (true) vs. user (false)
|
descendantUuid uuid references RbacReference (uuid) on delete cascade,
|
||||||
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:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
@ -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; $$;
|
||||||
|
|
||||||
|
@ -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; $$;
|
||||||
|
--/
|
@ -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,
|
-- @formatter:off
|
||||||
managed, assumed, empowered,
|
select o.objectTable || '#' || findIdNameByObjectUuid(o.objectTable, o.uuid) || '.' || r.roletype as grantedByRoleIdName,
|
||||||
ascendantUuid as userUuid,
|
g.objectTable || '#' || g.objectIdName || '.' || g.roletype as grantedRoleIdName, g.userName, g.assumed,
|
||||||
descendantUuid as roleUuid,
|
g.grantedByRoleUuid, g.descendantUuid as grantedRoleUuid, g.ascendantUuid as userUuid,
|
||||||
objectTable, objectUuid, objectIdName, roleType
|
g.objectTable, g.objectUuid, g.objectIdName, g.roleType as grantedRoleType
|
||||||
-- @formatter:off
|
from (
|
||||||
from (
|
select g.grantedbyroleuuid, g.ascendantuuid, g.descendantuuid, g.assumed,
|
||||||
select g.*, u.name as userName, o.objecttable, r.objectuuid, r.roletype,
|
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
|
||||||
-- @formatter:on
|
join RbacRole as r on r.uuid = grantedByRoleUuid
|
||||||
order by roleIdName;
|
join RbacObject as o on o.uuid = r.objectUuid
|
||||||
|
order by grantedRoleIdName;
|
||||||
|
-- @formatter:on
|
||||||
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; $$;
|
||||||
|
--//
|
@ -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; $$;
|
||||||
|
--//
|
||||||
|
|
||||||
|
|
||||||
|
@ -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;
|
||||||
$$;
|
$$;
|
||||||
--//
|
--//
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -7,51 +7,55 @@
|
|||||||
Creates test data for the package main table.
|
Creates test data for the package main table.
|
||||||
*/
|
*/
|
||||||
create or replace procedure createPackageTestData(
|
create or replace procedure createPackageTestData(
|
||||||
minCustomerReference integer, -- skip customers with reference below this
|
minCustomerReference integer, -- skip customers with reference below this
|
||||||
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;
|
||||||
pacName varchar;
|
custAdminUser varchar;
|
||||||
currentTask varchar;
|
custAdminRole varchar;
|
||||||
custAdmin varchar;
|
pacName varchar;
|
||||||
pac package;
|
currentTask varchar;
|
||||||
begin
|
pac package;
|
||||||
set hsadminng.currentUser to '';
|
begin
|
||||||
|
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)
|
||||||
values (cust.uuid, pacName, 'Here can add your own description of package ' || pacName || '.')
|
values (cust.uuid, pacName, 'Here can add your own description of package ' || pacName || '.')
|
||||||
returning * into pac;
|
returning * into pac;
|
||||||
|
|
||||||
call grantRoleToUser(
|
call grantRoleToUser(
|
||||||
findRoleId(packageAdmin(pac)),
|
getRoleId(customerAdmin(cust), 'fail'),
|
||||||
createRbacUser(pacName || '@' || cust.prefix || '.example.com'));
|
findRoleId(packageAdmin(pac)),
|
||||||
|
createRbacUser(pacName || '@' || cust.prefix || '.example.com'),
|
||||||
|
true);
|
||||||
|
|
||||||
end loop;
|
end loop;
|
||||||
end loop;
|
end loop;
|
||||||
|
|
||||||
if doCommitAfterEach then
|
if doCommitAfterEach then
|
||||||
commit;
|
commit;
|
||||||
end if;
|
end if;
|
||||||
end;
|
end ;
|
||||||
$$;
|
$$;
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
.contentType(ContentType.JSON)
|
.header("assumed-roles", givenAssumedRole)
|
||||||
.body("""
|
.contentType(ContentType.JSON)
|
||||||
{
|
.body("""
|
||||||
"userUuid": "%s",
|
{
|
||||||
"roleUuid": "%s",
|
"assumed": true,
|
||||||
"assumed": true,
|
"grantedRoleUuid": "%s",
|
||||||
"empowered": false
|
"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()
|
||||||
.statusCode(403);
|
.body("message", containsString("Access to granted role"))
|
||||||
|
.body("message", containsString("forbidden for {package#aaa00.admin}"))
|
||||||
|
.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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user