creating and viewing grants

This commit is contained in:
Michael Hoennig 2022-08-13 16:47:36 +02:00
parent c03697ccd9
commit 322736cd01
20 changed files with 817 additions and 32 deletions

View File

@ -461,12 +461,12 @@ actorHostmaster --> roleAdmins
As you can see, there something special: As you can see, there something special:
From the 'Role customer#xyz.owner' to the 'Role customer#xyz.admin' there is a dashed line, whereas all other lines are solid lines. From the 'Role customer#xyz.owner' to the 'Role customer#xyz.admin' there is a dashed line, whereas all other lines are solid lines.
Solid lines means, that one role is granted to another and followed in all queries to the restricted views. Solid lines means, that one role is granted to another and automatically assumed in all queries to the restricted views.
The dashed line means that one role is granted to another but not automatically followed in queries to the restricted views. The dashed line means that one role is granted to another but not automatically assumed in queries to the restricted views.
The reason here is that otherwise simply too many objects would be accessible to those with the 'administrators' role and all queries would be slowed down vastly. The reason here is that otherwise simply too many objects would be accessible to those with the 'administrators' role and all queries would be slowed down vastly.
Grants which are not followed are still valid grants for `hsadminng.assumedRoles`. Grants which are not automatically assumed are still valid grants for `hsadminng.assumedRoles`.
Thus, if you want to access anything below a customer, assume its role first. Thus, if you want to access anything below a customer, assume its role first.
There is actually another speciality in the customer roles: There is actually another speciality in the customer roles:
@ -632,4 +632,47 @@ The WHERE-IN-variant is about 50% slower on the smaller dataset, but almost keep
Both variants a viable option, depending on other needs, e.g. updatable views. Both variants a viable option, depending on other needs, e.g. updatable views.
## Access Control to RBAC-Objects
Access Control for business objects checked according to the assigned roles.
But we decided not to create such roles and permissions for the RBAC-Objects itself.
It would have overcomplicated the system and the necessary information can easily be added to the RBAC-Objects itself, mostly the `RbacGrant`s.
### RbacUser
Users can self-register, thus to create a new RbacUser entity, no login is required.
But such a user has no access-rights except viewing itself.
Users can view themselves.
And any user can view all other users as long as they have the same roles assigned.
As an exception, users which are assigned to global roles are not visible by other users.
At least an indirect lookup of known user-names (e.g. email address of the user) is possible
by users who have an empowered assignment of any role.
Otherwise, it would not be possible to assign roles to new users.
### RbacRole
All roles are system-defined and cannot be created or modified by any external API.
Users can view only the roles to which they are assigned.
## RbacGrant
Grant can be `empowered`, this means that the grantee user can grant the granted role to other users
and revoke grants to that role.
(TODO: access control part not yet implemented)
Grants can be `managed`, which means they are created and deleted by system-defined rules.
If a grant is not managed, it was created by an empowered user and can be deleted by empowered users.
Grants can be `assumed`, which means that they are immediately active.
If a grant is not assumed, the grantee user needs to use `assumeRoles` to activate it.
Users can see only grants of roles to which they are (directly?) assigned themselves.
TODO: If a user grants an indirect role to another user, that grant would not be visible to the user.
But if we make indirect grants visible, this would reveal too much information.
We also cannot keep the granting user in the grant because grants must survive deleted users,
e.g. if after an account was transferred to another user.

View File

@ -0,0 +1,64 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.generated.api.v1.api.RbacgrantsApi;
import net.hostsharing.hsadminng.generated.api.v1.api.RbacrolesApi;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacGrantResource;
import net.hostsharing.hsadminng.generated.api.v1.model.RbacRoleResource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import javax.transaction.Transactional;
import java.util.List;
import static net.hostsharing.hsadminng.Mapper.map;
import static net.hostsharing.hsadminng.Mapper.mapList;
@RestController
public class RbacGrantController implements RbacgrantsApi {
@Autowired
private Context context;
@Autowired
private RbacGrantRepository rbacGrantRepository;
@Override
@Transactional
public ResponseEntity<List<RbacGrantResource>> listUserGrants(
final String currentUser,
final String assumedRoles) {
context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
return ResponseEntity.ok(mapList(rbacGrantRepository.findAll(), RbacGrantResource.class));
}
@Override
@Transactional
public ResponseEntity<Void> grantRoleToUser(
final String currentUser,
final String assumedRoles,
final RbacGrantResource body) {
context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
rbacGrantRepository.save(map(body, RbacGrantEntity.class));
final var uri =
MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-grants/{roleUuid}")
.buildAndExpand(body.getRoleUuid())
.toUri();
return ResponseEntity.created(uri).build();
}
}

View File

@ -0,0 +1,60 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import lombok.*;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleType;
import org.springframework.data.annotation.Immutable;
import javax.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "rbacgrants_rv")
@IdClass(RbacGrantId.class)
@Getter
@Setter
@Builder
@ToString
@Immutable
@NoArgsConstructor
@AllArgsConstructor
public class RbacGrantEntity {
@Column(name = "username", updatable = false, insertable = false)
private String userName;
@Column(name = "roleidname", updatable = false, insertable = false)
private String roleIdName;
private boolean managed;
private boolean assumed;
private boolean empowered;
@Id
@Column(name = "useruuid")
private UUID userUuid;
@Id
@Column(name = "roleuuid")
private UUID roleUuid;
@Column(name = "objecttable", updatable = false, insertable = false)
private String objectTable;
@Column(name = "objectuuid", updatable = false, insertable = false)
private UUID objectUuid;
@Column(name = "objectidname", updatable = false, insertable = false)
private String objectIdName;
@Column(name = "roletype", updatable = false, insertable = false)
@Enumerated(EnumType.STRING)
private RbacRoleType roleType;
public String toDisplay() {
return "grant( " + userName + " -> " + roleIdName + ": " +
(managed ? "managed " : "") +
(assumed ? "assumed " : "") +
(empowered ? "empowered " : "") +
")";
}
}

View File

@ -0,0 +1,17 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.UUID;
@Getter
@EqualsAndHashCode
@NoArgsConstructor
public class RbacGrantId implements Serializable {
private UUID userUuid;
private UUID roleUuid;
}

View File

@ -0,0 +1,13 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import org.springframework.data.repository.Repository;
import java.util.List;
public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGrantId> {
List<RbacGrantEntity> findAll();
void save(final RbacGrantEntity grant);
}

View File

@ -24,11 +24,15 @@ public class RbacRoleController implements RbacrolesApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<List<RbacRoleResource>> listRoles(final String currentUser, final String assumedRoles) { public ResponseEntity<List<RbacRoleResource>> listRoles(
final String currentUser,
final String assumedRoles) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles); context.assumeRoles(assumedRoles);
} }
return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class)); return ResponseEntity.ok(mapList(rbacRoleRepository.findAll(), RbacRoleResource.class));
} }
} }

View File

@ -17,6 +17,9 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
""") """)
List<RbacUserEntity> findByOptionalNameLike(String userName); List<RbacUserEntity> findByOptionalNameLike(String userName);
@Query(value = "select uuid from rbacuser where name=:userName", nativeQuery = true)
UUID findUuidByName(String userName);
RbacUserEntity findByUuid(UUID uuid); RbacUserEntity findByUuid(UUID uuid);
@Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true) @Query(value = "select * from grantedPermissions(:userName)", nativeQuery = true)

View File

@ -0,0 +1,21 @@
components:
schemas:
RbacGrant:
type: object
properties:
userUuid:
type: string
format: uuid
roleUuid:
type: string
format: uuid
assumed:
type: boolean
empowered:
type: boolean
required:
- userUuid
- roleUuid

View File

@ -0,0 +1,39 @@
get:
tags:
- rbacgrants
operationId: listUserGrants
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
responses:
"200":
description: OK
content:
'application/json':
schema:
type: array
items:
$ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant'
post:
tags:
- rbacgrants
operationId: grantRoleToUser
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
requestBody:
required: true
content:
application/json:
schema:
$ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant'
responses:
"201":
description: OK
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
"409":
$ref: './api-definition/error-responses.yaml#/components/responses/Conflict'

View File

@ -66,7 +66,7 @@ create or replace function createRbacUser(refUuid uuid, userName varchar)
begin begin
insert insert
into RbacReference as r (uuid, type) into RbacReference as r (uuid, type)
values ( coalesce(refUuid, uuid_generate_v4()), 'RbacUser') values (coalesce(refUuid, uuid_generate_v4()), 'RbacUser')
returning r.uuid into refUuid; returning r.uuid into refUuid;
insert insert
into RbacUser (uuid, name) into RbacUser (uuid, name)
@ -206,6 +206,33 @@ begin
end; end;
$$; $$;
create or replace function findRoleId(roleIdName varchar)
returns uuid
returns null on null input
language plpgsql as $$
declare
roleParts text;
roleTypeFromRoleIdName RbacRoleType;
objectNameFromRoleIdName text;
objectTableFromRoleIdName text;
objectUuidOfRole uuid;
roleUuid uuid;
begin
-- TODO: extract function toRbacRoleDescriptor(roleIdName varchar) + find other occurrences
roleParts = overlay(roleIdName placing '#' from length(roleIdName) + 1 - strpos(reverse(roleIdName), '.'));
objectTableFromRoleIdName = split_part(roleParts, '#', 1);
objectNameFromRoleIdName = split_part(roleParts, '#', 2);
roleTypeFromRoleIdName = split_part(roleParts, '#', 3);
objectUuidOfRole = findObjectUuidByIdName(objectTableFromRoleIdName, objectNameFromRoleIdName);
select uuid
from RbacRole
where objectUuid = objectUuidOfRole
and roleType = roleTypeFromRoleIdName
into roleUuid;
return roleUuid;
end; $$;
create or replace function findRoleId(roleDescriptor RbacRoleDescriptor) create or replace function findRoleId(roleDescriptor RbacRoleDescriptor)
returns uuid returns uuid
returns null on null input returns null on null input
@ -322,13 +349,15 @@ $$;
--changeset rbac-base-GRANTS:1 endDelimiter:--// --changeset rbac-base-GRANTS:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
/* /*
Table to store grants / role- or permission assignments to users or roles.
*/ */
create table RbacGrants create table RbacGrants
( (
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,
follow boolean not null default true, managed boolean not null default false, -- created by system (true) vs. user (false)
assumed boolean not null default true, -- auto assumed (true) vs. needs assumeRoles (false)
empowered boolean not null default false, -- true: allows grant+revoke for descendant role
primary key (ascendantUuid, descendantUuid) primary key (ascendantUuid, descendantUuid)
); );
create index on RbacGrants (ascendantUuid); create index on RbacGrants (ascendantUuid);
@ -377,7 +406,8 @@ declare
granteeId uuid; granteeId uuid;
begin begin
-- TODO: needs optimization -- TODO: needs optimization
foreach granteeId in array granteeIds loop foreach granteeId in array granteeIds
loop
if isGranted(granteeId, grantedId) then if isGranted(granteeId, grantedId) then
return true; return true;
end if; end if;
@ -416,7 +446,8 @@ select exists(
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 as o on o.uuid = r.objectuuid join RbacObject as o on o.uuid = r.objectuuid
where g.ascendantuuid = userUuid and o.objecttable = 'global' where g.ascendantuuid = userUuid
and o.objecttable = 'global'
); );
$$; $$;
@ -432,14 +463,14 @@ begin
perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission'); perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
insert insert
into RbacGrants (ascendantUuid, descendantUuid, follow) into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (roleUuid, permissionIds[i], true) values (roleUuid, permissionIds[i], true, true, false)
on conflict do nothing; -- allow granting multiple times on conflict do nothing; -- allow granting multiple times
end loop; end loop;
end; end;
$$; $$;
create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doFollow bool = true) create or replace procedure grantRoleToRole(subRoleId uuid, superRoleId uuid, doAssume bool = true)
language plpgsql as $$ language plpgsql as $$
begin begin
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole'); perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
@ -450,8 +481,8 @@ begin
end if; end if;
insert insert
into RbacGrants (ascendantUuid, descendantUuid, follow) into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (superRoleId, subRoleId, doFollow) values (superRoleId, subRoleId, true, doAssume, false)
on conflict do nothing; -- allow granting multiple times on conflict do nothing; -- allow granting multiple times
end; $$; end; $$;
@ -466,16 +497,45 @@ begin
end if; end if;
end; $$; end; $$;
create or replace procedure grantRoleToUser(roleId uuid, userId uuid) create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid)
language plpgsql as $$ language plpgsql as $$
begin begin
perform assertReferenceType('roleId (ascendant)', roleId, 'RbacRole'); perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole');
perform assertReferenceType('userId (descendant)', userId, 'RbacUser'); perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser');
insert insert
into RbacGrants (ascendantUuid, descendantUuid, follow) into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (userId, roleId, true) values (userUuid, roleUuid, true, true, true);
on conflict do nothing; -- allow granting multiple times -- 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; $$; end; $$;
--// --//
@ -499,14 +559,14 @@ begin
return query select distinct perm.objectUuid return query select distinct perm.objectUuid
from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level
from RbacGrants from RbacGrants
where follow where assumed
and ascendantUuid = any (subjectIds) and ascendantUuid = any (subjectIds)
union union
distinct distinct
select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level
from RbacGrants "grant" from RbacGrants "grant"
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid inner join grants recur on recur.descendantUuid = "grant".ascendantUuid
where follow) where assumed)
select descendantUuid select descendantUuid
from grants) as granted from grants) as granted
join RbacPermission perm join RbacPermission perm
@ -536,7 +596,7 @@ create or replace function queryPermissionsGrantedToSubjectId(subjectId uuid)
returns setof RbacPermission returns setof RbacPermission
strict strict
language sql as $$ language sql as $$
-- @formatter:off -- @formatter:off
select * select *
from RbacPermission from RbacPermission
where uuid in ( where uuid in (

View File

@ -24,6 +24,72 @@ grant all privileges on rbacrole_rv to restricted;
--// --//
-- ============================================================================
--changeset rbac-views-GRANT-RESTRICTED-VIEW:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Creates a view to the grants table with row-level limitation
based on the direct grants of the current user.
*/
drop view if exists rbacgrants_rv;
create or replace view rbacgrants_rv as
select userName, objectTable||'#'||objectIdName||'.'||roletype as roleIdName,
managed, assumed, empowered,
ascendantUuid as userUuid,
descendantUuid as roleUuid,
objectTable, objectUuid, objectIdName, roleType
-- @formatter:off
from (
select g.*, u.name as userName, o.objecttable, r.objectuuid, r.roletype,
findIdNameByObjectUuid(o.objectTable, o.uuid) as objectIdName
from rbacgrants as g
join rbacrole as r on r.uuid = g.descendantUuid
join rbacobject o on o.uuid = r.objectuuid
join rbacuser u on u.uuid = g.ascendantuuid
where isGranted(currentSubjectIds(), r.uuid)
) as unordered
-- @formatter:on
order by objectIdName;
grant all privileges on rbacrole_rv to restricted;
--//
-- ============================================================================
--changeset rbac-views-GRANTS-RV-INSERT-TRIGGER:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/**
Instead of insert trigger function for RbacGrants_RV.
*/
create or replace function insertRbacGrant()
returns trigger
language plpgsql as $$
declare
newGrant RbacGrants_RV;
begin
if new.managed then
raise exception '[400] Managed grants cannot be inserted via RBacGrants_RV.';
end if;
call grantRoleToUser(new.roleUuid, new.userUuid,
ROW(false, new.assumed, new.empowered));
select grv.*
from RbacGrants_RV grv
where grv.userUuid=new.userUuid and grv.roleUuid=new.roleUuid
into newGrant;
return newGrant;
end; $$;
/*
Creates an instead of insert trigger for the RbacGrants_rv view.
*/
create trigger insertRbacGrant_Trigger
instead of insert
on RbacGrants_rv
for each row
execute function insertRbacGrant();
-- ============================================================================ -- ============================================================================
--changeset rbac-views-USER-RESTRICTED-VIEW:1 endDelimiter:--// --changeset rbac-views-USER-RESTRICTED-VIEW:1 endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------

View File

@ -76,7 +76,7 @@ begin
customerAdminUuid = createRole( customerAdminUuid = createRole(
customerAdmin(NEW), customerAdmin(NEW),
grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']), grantingPermissions(forObjectUuid => NEW.uuid, permitOps => array ['view', 'add-package']),
-- NO auto follow 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
); );

View File

@ -0,0 +1,13 @@
package net.hostsharing.hsadminng;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface Accepts {
String[] value();
}

View File

@ -32,7 +32,7 @@ class PackageRepositoryIntegrationTest {
class FindAllByOptionalNameLike { class FindAllByOptionalNameLike {
@Test @Test
public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() { public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
// given // given
currentUser("mike@hostsharing.net"); currentUser("mike@hostsharing.net");
@ -44,7 +44,7 @@ class PackageRepositoryIntegrationTest {
} }
@Test @Test
public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() { public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
given: given:
currentUser("mike@hostsharing.net"); currentUser("mike@hostsharing.net");
assumedRoles("global#hostsharing.admin"); assumedRoles("global#hostsharing.admin");

View File

@ -0,0 +1,168 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleEntity;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserEntity;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import net.hostsharing.test.JpaAttempt;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { HsadminNgApplication.class, JpaAttempt.class }
)
@Accepts({ "ROL:S(Schema)" })
class RbacGrantControllerAcceptanceTest {
@LocalServerPort
Integer port;
@Autowired
EntityManager em;
@Autowired
Context context;
@Autowired
RbacUserRepository rbacUserRepository;
@Autowired
RbacRoleRepository rbacRoleRepository;
@Autowired
RbacGrantRepository rbacGrantRepository;
@Autowired
JpaAttempt jpaAttempt;
@Test
@Accepts({ "ROL:L(List)" })
void returnsRbacGrantsForPackageAdmin() {
RestAssured // @formatter:off
.given()
.header("current-user", "aaa00@aaa.example.com")
.port(port)
.when()
.get("http://localhost/api/rbac-roles")
.then().assertThat()
.statusCode(200)
.contentType("application/json")
.body("[0].roleName", is("customer#aaa.tenant"))
.body("[1].roleName", is("package#aaa00.admin"))
.body("[2].roleName", is("package#aaa00.tenant"));
// @formatter:on
}
@Test
@Accepts({ "ROL:C(Create)" })
void packageAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final var givenOwnPackageAdminRole = "package#aaa00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenOwnPackageAdminRole).getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/rbac-grants")
.then().assertThat()
.statusCode(201);
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( " + givenNewUserName + " -> " + givenOwnPackageAdminRole + ": assumed )");
}
@Test
@Accepts({ "ROL:C(Create)", "ROL:X(Access Control)" })
void packageAdmin_canNotGrantAlienPackageAdminRole_toArbitraryUser() {
// given
final var givenNewUserName = "test-user-" + RandomStringUtils.randomAlphabetic(8) + "@example.com";
final String givenPackageAdmin = "aaa00@aaa.example.com";
final var givenAlienPackageAdminRole = "package#aab00.admin";
// when
RestAssured // @formatter:off
.given()
.header("current-user", givenPackageAdmin)
.contentType(ContentType.JSON)
.body("""
{
"userUuid": "%s",
"roleUuid": "%s",
"assumed": true,
"empowered": false
}
""".formatted(
createRBacUser(givenNewUserName).getUuid().toString(),
findRbacRoleByName(givenAlienPackageAdminRole).getUuid().toString())
)
.port(port)
.when()
.post("http://localhost/api/rbac-grants")
.then().assertThat()
.statusCode(403);
// @formatter:on
// then
assertThat(findAllGrantsOfUser(givenPackageAdmin))
.extracting(RbacGrantEntity::getUserName)
.doesNotContain(givenNewUserName);
}
List<RbacGrantEntity> findAllGrantsOfUser(final String userName) {
return jpaAttempt.transacted(() -> {
context.setCurrentUser(userName);
return rbacGrantRepository.findAll();
}).returnedValue();
}
RbacUserEntity createRBacUser(final String userName) {
return jpaAttempt.transacted(() -> {
return rbacUserRepository.create(new RbacUserEntity(UUID.randomUUID(), userName));
}).returnedValue();
}
RbacRoleEntity findRbacRoleByName(final String roleName) {
return jpaAttempt.transacted(() -> {
context.setCurrentUser("mike@hostsharing.net");
return rbacRoleRepository.findByRoleName(roleName);
}).returnedValue();
}
}

View File

@ -0,0 +1,142 @@
package net.hostsharing.hsadminng.rbac.rbacgrant;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacrole.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.test.annotation.DirtiesContext;
import javax.persistence.EntityManager;
import java.util.List;
import static net.hostsharing.test.JpaAttempt.attempt;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
@ComponentScan(basePackageClasses = { Context.class, RbacGrantRepository.class })
@DirtiesContext
@Accepts({ "GRT:S(Schema)" })
class RbacGrantRepositoryIntegrationTest {
@Autowired
Context context;
@Autowired
RbacGrantRepository rbacGrantRepository;
@Autowired
RbacUserRepository rbacUserRepository;
@Autowired
RbacRoleRepository rbacRoleRepository;
@Autowired
EntityManager em;
@Nested
class FindAllRbacGrants {
@Test
@Accepts({ "GRT:L(List)" })
public void packageAdmin_canViewItsRbacGrants() {
// given
currentUser("aaa00@aaa.example.com");
// when
final var result = rbacGrantRepository.findAll();
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )");
}
@Test
@Accepts({ "GRT:L(List)" })
public void customerAdmin_canViewItsRbacGrants() {
// given
currentUser("admin@aaa.example.com");
// when
final var result = rbacGrantRepository.findAll();
// then
exactlyTheseRbacGrantsAreReturned(
result,
"grant( admin@aaa.example.com -> customer#aaa.admin: managed assumed empowered )",
"grant( aaa00@aaa.example.com -> package#aaa00.admin: managed assumed empowered )",
"grant( aaa01@aaa.example.com -> package#aaa01.admin: managed assumed empowered )",
"grant( aaa02@aaa.example.com -> package#aaa02.admin: managed assumed empowered )");
}
@Test
@Accepts({ "GRT:L(List)" })
public void customerAdmin_withAssumedRole_cannotViewRbacGrants() {
// given:
currentUser("admin@aaa.example.com");
assumedRoles("package#aab00.admin");
// when
final var result = attempt(
em,
() -> rbacGrantRepository.findAll());
// then
result.assertExceptionWithRootCauseMessage(
JpaSystemException.class,
"[403] user admin@aaa.example.com", "has no permission to assume role package#aab00#admin");
}
}
@Nested
class CreateRbacGrant {
@Test
@Accepts({ "GRT:C(Create)" })
public void customerAdmin_canGrantOwnPackageAdminRole_toArbitraryUser() {
// given
currentUser("admin@aaa.example.com");
final var userUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com");
final var roleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
// when
final var grant = RbacGrantEntity.builder()
.userUuid(userUuid).roleUuid(roleUuid)
.assumed(true).empowered(false)
.build();
final var attempt = attempt(em, () ->
rbacGrantRepository.save(grant)
);
// then
assertThat(attempt.wasSuccessful()).isTrue();
assertThat(rbacGrantRepository.findAll())
.extracting(RbacGrantEntity::toDisplay)
.contains("grant( aac00@aac.example.com -> package#aaa00.admin: assumed )");
}
}
void currentUser(final String currentUser) {
context.setCurrentUser(currentUser);
assertThat(context.getCurrentUser()).as("precondition").isEqualTo(currentUser);
}
void assumedRoles(final String assumedRoles) {
context.assumeRoles(assumedRoles);
assertThat(context.getAssumedRoles()).as("precondition").containsExactly(assumedRoles.split(";"));
}
void exactlyTheseRbacGrantsAreReturned(final List<RbacGrantEntity> actualResult, final String... expectedGrant) {
assertThat(actualResult)
.filteredOn(g -> !g.getUserName().startsWith("test-user-")) // ignore test-users created by other tests
.extracting(RbacGrantEntity::toDisplay)
.containsExactlyInAnyOrder(expectedGrant);
}
}

View File

@ -0,0 +1,60 @@
package net.hostsharing.hsadminng.rbac.rbacrole;
import io.restassured.RestAssured;
import net.hostsharing.hsadminng.Accepts;
import net.hostsharing.hsadminng.HsadminNgApplication;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.rbacuser.RbacUserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import javax.persistence.EntityManager;
import static org.hamcrest.Matchers.is;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = HsadminNgApplication.class
)
@Accepts({ "ROL:*:S:Schema" })
class RbacRoleControllerAcceptanceTest {
@LocalServerPort
private Integer port;
@Autowired
EntityManager em;
@Autowired
Context context;
@Autowired
RbacUserRepository rbacUserRepository;
@Autowired
RbacRoleRepository rbacRoleRepository;
@Test
@Accepts({ "ROL:*:L:List" })
void returnsRbacRolesForAssumedPackageAdmin() {
// @formatter:off
RestAssured
.given()
.header("current-user", "mike@hostsharing.net")
.header("assumed-roles", "package#aaa00.admin")
.port(port)
.when()
.get("http://localhost/api/rbac-roles")
.then().assertThat()
.statusCode(200)
.contentType("application/json")
.body("[0].roleName", is("customer#aaa.tenant"))
.body("[1].roleName", is("package#aaa00.admin"))
.body("[2].roleName", is("package#aaa00.tenant"));
// @formatter:on
}
}

View File

@ -15,8 +15,7 @@ import javax.transaction.Transactional;
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.Matchers.*;
import static org.hamcrest.Matchers.startsWith;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
@ -56,7 +55,12 @@ class RbacUserControllerAcceptanceTest {
.body("[0].name", is("aaa00@aaa.example.com")) .body("[0].name", is("aaa00@aaa.example.com"))
.body("[1].name", is("aaa01@aaa.example.com")) .body("[1].name", is("aaa01@aaa.example.com"))
.body("[2].name", is("aaa02@aaa.example.com")) .body("[2].name", is("aaa02@aaa.example.com"))
.body("size()", is(14)); .body("[3].name", is("aab00@aab.example.com"))
// ...
.body("[11].name", is("admin@aac.example.com"))
.body("[12].name", is("mike@hostsharing.net"))
.body("[13].name", is("sven@hostsharing.net"))
.body("size()", greaterThanOrEqualTo(14));
// @formatter:on // @formatter:on
} }

View File

@ -371,6 +371,7 @@ class RbacUserRepositoryIntegrationTest {
void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) { void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) {
assertThat(actualResult) assertThat(actualResult)
.filteredOn(u -> !u.getName().startsWith("test-user-"))
.extracting(RbacUserEntity::getName) .extracting(RbacUserEntity::getName)
.containsExactlyInAnyOrder(expectedUserNames); .containsExactlyInAnyOrder(expectedUserNames);
} }

View File

@ -49,6 +49,13 @@ public class JpaAttempt {
} }
} }
public static JpaResult<Void> attempt(final EntityManager em, final Runnable code) {
return attempt(em, () -> {
code.run();
return null;
});
}
@Transactional(propagation = Propagation.REQUIRES_NEW) @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); return attempt(em, code);