1
0

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

@ -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
@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);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
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);
@Query(value = "select uuid from rbacuser where name=:userName", nativeQuery = true)
UUID findUuidByName(String userName);
RbacUserEntity findByUuid(UUID uuid);
@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
insert
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;
insert
into RbacUser (uuid, name)
@ -206,6 +206,33 @@ begin
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)
returns uuid
returns null on null input
@ -322,13 +349,15 @@ $$;
--changeset rbac-base-GRANTS:1 endDelimiter:--//
-- ----------------------------------------------------------------------------
/*
Table to store grants / role- or permission assignments to users or roles.
*/
create table RbacGrants
(
ascendantUuid 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)
);
create index on RbacGrants (ascendantUuid);
@ -377,7 +406,8 @@ declare
granteeId uuid;
begin
-- TODO: needs optimization
foreach granteeId in array granteeIds loop
foreach granteeId in array granteeIds
loop
if isGranted(granteeId, grantedId) then
return true;
end if;
@ -413,10 +443,11 @@ create or replace function hasGlobalRoleGranted(userUuid uuid)
language sql as $$
select exists(
select r.uuid
from RbacGrants as g
join RbacRole as r on r.uuid = g.descendantuuid
join RbacObject as o on o.uuid = r.objectuuid
where g.ascendantuuid = userUuid and o.objecttable = 'global'
from RbacGrants as g
join RbacRole as r on r.uuid = g.descendantuuid
join RbacObject as o on o.uuid = r.objectuuid
where g.ascendantuuid = userUuid
and o.objecttable = 'global'
);
$$;
@ -432,14 +463,14 @@ begin
perform assertReferenceType('permissionId (descendant)', permissionIds[i], 'RbacPermission');
insert
into RbacGrants (ascendantUuid, descendantUuid, follow)
values (roleUuid, permissionIds[i], true)
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (roleUuid, permissionIds[i], true, true, false)
on conflict do nothing; -- allow granting multiple times
end loop;
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 $$
begin
perform assertReferenceType('superRoleId (ascendant)', superRoleId, 'RbacRole');
@ -450,8 +481,8 @@ begin
end if;
insert
into RbacGrants (ascendantUuid, descendantUuid, follow)
values (superRoleId, subRoleId, doFollow)
into RbacGrants (ascendantUuid, descendantUuid, managed, assumed, empowered)
values (superRoleId, subRoleId, true, doAssume, false)
on conflict do nothing; -- allow granting multiple times
end; $$;
@ -466,16 +497,45 @@ begin
end if;
end; $$;
create or replace procedure grantRoleToUser(roleId uuid, userId uuid)
create or replace procedure grantRoleToUser(roleUuid uuid, userUuid uuid)
language plpgsql as $$
begin
perform assertReferenceType('roleId (ascendant)', roleId, 'RbacRole');
perform assertReferenceType('userId (descendant)', userId, 'RbacUser');
perform assertReferenceType('roleId (descendant)', roleUuid, 'RbacRole');
perform assertReferenceType('userId (ascendant)', userUuid, 'RbacUser');
insert
into RbacGrants (ascendantUuid, descendantUuid, follow)
values (userId, roleId, true)
on conflict do nothing; -- allow granting multiple times
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; $$;
--//
@ -499,14 +559,14 @@ begin
return query select distinct perm.objectUuid
from (with recursive grants as (select descendantUuid, ascendantUuid, 1 as level
from RbacGrants
where follow
where assumed
and ascendantUuid = any (subjectIds)
union
distinct
select "grant".descendantUuid, "grant".ascendantUuid, level + 1 as level
from RbacGrants "grant"
inner join grants recur on recur.descendantUuid = "grant".ascendantUuid
where follow)
where assumed)
select descendantUuid
from grants) as granted
join RbacPermission perm
@ -536,7 +596,7 @@ create or replace function queryPermissionsGrantedToSubjectId(subjectId uuid)
returns setof RbacPermission
strict
language sql as $$
-- @formatter:off
-- @formatter:off
select *
from RbacPermission
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:--//
-- ----------------------------------------------------------------------------

View File

@ -76,7 +76,7 @@ begin
customerAdminUuid = createRole(
customerAdmin(NEW),
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
);

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 {
@Test
public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() {
public void hostsharingAdmin_withoutAssumedRole_canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
// given
currentUser("mike@hostsharing.net");
@ -44,7 +44,7 @@ class PackageRepositoryIntegrationTest {
}
@Test
public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotFollowed() {
public void hostsharingAdmin_withAssumedHostsharingAdminRole__canNotViewAnyPackages_becauseThoseGrantsAreNotassumedd() {
given:
currentUser("mike@hostsharing.net");
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 static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.startsWith;
import static org.hamcrest.Matchers.*;
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
@ -56,7 +55,12 @@ class RbacUserControllerAcceptanceTest {
.body("[0].name", is("aaa00@aaa.example.com"))
.body("[1].name", is("aaa01@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
}

View File

@ -371,6 +371,7 @@ class RbacUserRepositoryIntegrationTest {
void exactlyTheseRbacUsersAreReturned(final List<RbacUserEntity> actualResult, final String... expectedUserNames) {
assertThat(actualResult)
.filteredOn(u -> !u.getName().startsWith("test-user-"))
.extracting(RbacUserEntity::getName)
.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)
public <T> JpaResult<T> transacted(final Supplier<T> code) {
return attempt(em, code);