implements rbac-grants get-by-id

This commit is contained in:
Michael Hoennig 2022-08-19 17:39:41 +02:00
parent a66ed8e59f
commit 5ea8069608
14 changed files with 292 additions and 78 deletions

View File

@ -25,11 +25,32 @@ public class RbacGrantController implements RbacgrantsApi {
@Autowired @Autowired
private RbacGrantRepository rbacGrantRepository; private RbacGrantRepository rbacGrantRepository;
@Override
@Transactional(readOnly = true)
public ResponseEntity<RbacGrantResource> getGrantById(
final String currentUser,
final String assumedRoles,
final UUID grantedRoleUuid,
final UUID granteeUserUuid) {
context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) {
context.assumeRoles(assumedRoles);
}
final var id = new RbacGrantId(granteeUserUuid, grantedRoleUuid);
final var result = rbacGrantRepository.findById(id);
if (result == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(map(result, RbacGrantResource.class));
}
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public ResponseEntity<List<RbacGrantResource>> listUserGrants( public ResponseEntity<List<RbacGrantResource>> listUserGrants(
final String currentUser, final String currentUser,
final String assumedRoles) { final String assumedRoles) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {
@ -41,9 +62,9 @@ public class RbacGrantController implements RbacgrantsApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> grantRoleToUser( public ResponseEntity<Void> grantRoleToUser(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,
final RbacGrantResource body) { final RbacGrantResource body) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {
@ -53,20 +74,20 @@ public class RbacGrantController implements RbacgrantsApi {
rbacGrantRepository.save(map(body, RbacGrantEntity.class)); rbacGrantRepository.save(map(body, RbacGrantEntity.class));
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-grants/{roleUuid}") .path("/api/rbac-grants/{roleUuid}")
.buildAndExpand(body.getGrantedRoleUuid()) .buildAndExpand(body.getGrantedRoleUuid())
.toUri(); .toUri();
return ResponseEntity.created(uri).build(); return ResponseEntity.created(uri).build();
} }
@Override @Override
@Transactional @Transactional
public ResponseEntity<Void> revokeRoleFromUser( public ResponseEntity<Void> revokeRoleFromUser(
final String currentUser, final String currentUser,
final String assumedRoles, final String assumedRoles,
final UUID grantedRoleUuid, final UUID grantedRoleUuid,
final UUID granteeUserUuid) { final UUID granteeUserUuid) {
context.setCurrentUser(currentUser); context.setCurrentUser(currentUser);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {

View File

@ -18,6 +18,7 @@ import java.util.UUID;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class RbacGrantEntity { public class RbacGrantEntity {
@Column(name = "grantedbyroleidname", updatable = false, insertable = false) @Column(name = "grantedbyroleidname", updatable = false, insertable = false)
private String grantedByRoleIdName; private String grantedByRoleIdName;
@ -59,6 +60,6 @@ public class RbacGrantEntity {
public String toDisplay() { public String toDisplay() {
return "{ grant " + (assumed ? "assumed " : "") + return "{ grant " + (assumed ? "assumed " : "") +
"role " + grantedRoleIdName + " to user " + granteeUserName + " by role " + grantedByRoleIdName + " }"; "role " + grantedRoleIdName + " to user " + granteeUserName + " by role " + grantedByRoleIdName + " }";
} }
} }

View File

@ -8,15 +8,22 @@ import java.util.List;
public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGrantId> { public interface RbacGrantRepository extends Repository<RbacGrantEntity, RbacGrantId> {
@Query(value = """
select g from RbacGrantEntity as g
where g.grantedRoleUuid=:#{#rbacGrantId.grantedRoleUuid}
and g.granteeUserUuid=:#{#rbacGrantId.granteeUserUuid}
""")
RbacGrantEntity findById(RbacGrantId rbacGrantId);
List<RbacGrantEntity> findAll(); List<RbacGrantEntity> findAll();
void save(final RbacGrantEntity grant); void save(final RbacGrantEntity grant);
@Modifying @Modifying
@Query(value = """ @Query(value = """
delete from RbacGrantEntity as g delete from RbacGrantEntity as g
where g.grantedRoleUuid=:#{#rbacGrantId.grantedRoleUuid} where g.grantedRoleUuid=:#{#rbacGrantId.grantedRoleUuid}
and g.granteeUserUuid=:#{#rbacGrantId.granteeUserUuid} and g.granteeUserUuid=:#{#rbacGrantId.granteeUserUuid}
""") """)
void deleteByRbacGrantId(RbacGrantId rbacGrantId); void deleteByRbacGrantId(RbacGrantId rbacGrantId);
} }

View File

@ -32,7 +32,7 @@ public class RbacUserController implements RbacusersApi {
@Override @Override
@Transactional @Transactional
public ResponseEntity<RbacUserResource> createUser( public ResponseEntity<RbacUserResource> createUser(
@RequestBody final RbacUserResource body @RequestBody final RbacUserResource body
) { ) {
if (body.getUuid() == null) { if (body.getUuid() == null) {
body.setUuid(UUID.randomUUID()); body.setUuid(UUID.randomUUID());
@ -40,19 +40,27 @@ public class RbacUserController implements RbacusersApi {
final var saved = map(body, RbacUserEntity.class); final var saved = map(body, RbacUserEntity.class);
rbacUserRepository.create(saved); rbacUserRepository.create(saved);
final var uri = final var uri =
MvcUriComponentsBuilder.fromController(getClass()) MvcUriComponentsBuilder.fromController(getClass())
.path("/api/rbac-users/{id}") .path("/api/rbac-users/{id}")
.buildAndExpand(saved.getUuid()) .buildAndExpand(saved.getUuid())
.toUri(); .toUri();
return ResponseEntity.created(uri).body(map(saved, RbacUserResource.class)); return ResponseEntity.created(uri).body(map(saved, RbacUserResource.class));
} }
@Override @Override
@Transactional(readOnly=true) public ResponseEntity<List<RbacUserPermissionResource>> getUserById(
final String currentUser,
final String assumedRoles,
final String userName) {
return null;
}
@Override
@Transactional(readOnly = true)
public ResponseEntity<List<RbacUserResource>> listUsers( public ResponseEntity<List<RbacUserResource>> listUsers(
@RequestHeader(name = "current-user") final String currentUserName, @RequestHeader(name = "current-user") final String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, @RequestHeader(name = "assumed-roles", required = false) final String assumedRoles,
@RequestParam(name = "name", required = false) final String userName @RequestParam(name = "name", required = false) final String userName
) { ) {
context.setCurrentUser(currentUserName); context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {
@ -62,11 +70,11 @@ public class RbacUserController implements RbacusersApi {
} }
@Override @Override
@Transactional(readOnly=true) @Transactional(readOnly = true)
public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions( public ResponseEntity<List<RbacUserPermissionResource>> listUserPermissions(
@RequestHeader(name = "current-user") final String currentUserName, @RequestHeader(name = "current-user") final String currentUserName,
@RequestHeader(name = "assumed-roles", required = false) final String assumedRoles, @RequestHeader(name = "assumed-roles", required = false) final String assumedRoles,
@PathVariable(name = "userName") final String userName @PathVariable(name = "userName") final String userName
) { ) {
context.setCurrentUser(currentUserName); context.setCurrentUser(currentUserName);
if (assumedRoles != null && !assumedRoles.isBlank()) { if (assumedRoles != null && !assumedRoles.isBlank()) {

View File

@ -3,7 +3,6 @@ package net.hostsharing.hsadminng.rbac.rbacuser;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -11,14 +10,15 @@ import java.util.UUID;
public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> { public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
@Query(""" @Query("""
select u from RbacUserEntity u select u from RbacUserEntity u
where :userName is null or u.name like concat(:userName, '%') where :userName is null or u.name like concat(:userName, '%')
order by u.name order by u.name
""") """)
List<RbacUserEntity> findByOptionalNameLike(String userName); List<RbacUserEntity> findByOptionalNameLike(String userName);
@Query(value = "select uuid from rbacuser where name=:userName", nativeQuery = true) // bypasses the restricted view, to be able to grant rights to arbitrary user
UUID findUuidByName(String userName); @Query(value = "select * from rbacuser where name=:userName", nativeQuery = true)
RbacUserEntity findByName(String userName);
RbacUserEntity findByUuid(UUID uuid); RbacUserEntity findByUuid(UUID uuid);
@ -32,7 +32,7 @@ public interface RbacUserRepository extends Repository<RbacUserEntity, UUID> {
*/ */
@Modifying @Modifying
@Query(value = "insert into RBacUser_RV (uuid, name) values( :#{#newUser.uuid}, :#{#newUser.name})", nativeQuery = true) @Query(value = "insert into RBacUser_RV (uuid, name) values( :#{#newUser.uuid}, :#{#newUser.name})", nativeQuery = true)
void insert(@Param("newUser") final RbacUserEntity newUser); void insert(final RbacUserEntity newUser);
default RbacUserEntity create(final RbacUserEntity rbacUserEntity) { default RbacUserEntity create(final RbacUserEntity rbacUserEntity) {
if (rbacUserEntity.getUuid() == null) { if (rbacUserEntity.getUuid() == null) {

View File

@ -13,8 +13,11 @@ paths:
/api/rbac-users: /api/rbac-users:
$ref: "./api-definition/rbac-users.yaml" $ref: "./api-definition/rbac-users.yaml"
/api/rbac-users/{userName}/permissions: /api/rbac-users/{userUuid}:
$ref: "./api-definition/rbac-users-permissions.yaml" $ref: "./api-definition/rbac-users-with-id.yaml"
/api/rbac-users/{userUuid}/permissions:
$ref: "./api-definition/rbac-users-with-id-permissions.yaml"
/api/rbac-roles: /api/rbac-roles:
$ref: "./api-definition/rbac-roles.yaml" $ref: "./api-definition/rbac-roles.yaml"
@ -23,7 +26,7 @@ paths:
$ref: "./api-definition/rbac-grants.yaml" $ref: "./api-definition/rbac-grants.yaml"
/api/rbac-grants/{grantedRoleUuid}/{granteeUserUuid}: /api/rbac-grants/{grantedRoleUuid}/{granteeUserUuid}:
$ref: "./api-definition/rbac-grants-id.yaml" $ref: "./api-definition/rbac-grants-with-id.yaml"
# HS # HS

View File

@ -6,11 +6,20 @@ components:
RbacGrant: RbacGrant:
type: object type: object
properties: properties:
grantedByRoleIdName:
type: string
grantedByRoleUuid:
type: string
format: uuid
assumed: assumed:
type: boolean type: boolean
grantedRoleIdName:
type: string
grantedRoleUuid: grantedRoleUuid:
type: string type: string
format: uuid format: uuid
granteeUserName:
type: string
granteeUserUuid: granteeUserUuid:
type: string type: string
format: uuid format: uuid

View File

@ -1,30 +0,0 @@
delete:
tags:
- rbacgrants
operationId: revokeRoleFromUser
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: grantedRoleUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the granted role.
- name: granteeUserUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the user to whom the role was granted.
responses:
"204":
description: No Content
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
"404":
$ref: './api-definition/error-responses.yaml#/components/responses/NotFound'

View File

@ -0,0 +1,65 @@
get:
tags:
- rbacgrants
operationId: getGrantById
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: grantedRoleUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the granted role.
- name: granteeUserUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the user to whom the role was granted.
responses:
"200":
description: OK
content:
'application/json':
schema:
$ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant'
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
"404":
$ref: './api-definition/error-responses.yaml#/components/responses/NotFound'
delete:
tags:
- rbacgrants
operationId: revokeRoleFromUser
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: grantedRoleUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the granted role.
- name: granteeUserUuid
in: path
required: true
schema:
type: string
format: uuid
description: UUID of the user to whom the role was granted.
responses:
"204":
description: No Content
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'
"404":
$ref: './api-definition/error-responses.yaml#/components/responses/NotFound'

View File

@ -13,7 +13,7 @@ get:
schema: schema:
type: array type: array
items: items:
$ref: './api-definition/rbac-grant-schemas.yaml#/components/schemas/RbacGrant' $ref: './rbac-grant-schemas.yaml#/components/schemas/RbacGrant'
post: post:
tags: tags:

View File

@ -0,0 +1,27 @@
get:
tags:
- rbacusers
description: 'Fetch a single user by its id, if visible for the current subject.'
operationId: getUserById
parameters:
- $ref: './api-definition/auth.yaml#/components/parameters/currentUser'
- $ref: './api-definition/auth.yaml#/components/parameters/assumedRoles'
- name: userName
in: path
required: true
schema:
type: string
responses:
"200":
description: OK
content:
'application/json':
schema:
type: array
items:
$ref: './api-definition/rbac-user-schemas.yaml#/components/schemas/RbacUserPermission'
"401":
$ref: './api-definition/error-responses.yaml#/components/responses/Unauthorized'
"403":
$ref: './api-definition/error-responses.yaml#/components/responses/Forbidden'

View File

@ -27,6 +27,7 @@ import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat; import static org.assertj.core.api.Assumptions.assumeThat;
import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.is;
@SpringBootTest( @SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
@ -57,6 +58,67 @@ class RbacGrantControllerAcceptanceTest {
@Autowired @Autowired
JpaAttempt jpaAttempt; JpaAttempt jpaAttempt;
@Nested
class GetGrantById {
@Test
@Accepts({ "GRT:R(Read)" })
void customerAdmin_withAssumedPacketAdminRole_canReadPacketAdminsGrantById() {
// given
final var givenCurrentUserAsPackageAdmin = new Subject("admin@aaa.example.com");
final var givenGranteeUser = findRbacUserByName("aaa00@aaa.example.com");
final var givenGrantedRole = findRbacRoleByName("package#aaa00.admin");
// when
final var grant = givenCurrentUserAsPackageAdmin.getGrantById()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(200)
.body("grantedByRoleIdName", is("customer#aaa.admin"))
.body("grantedRoleIdName", is("package#aaa00.admin"))
.body("granteeUserName", is("aaa00@aaa.example.com"));
}
@Test
@Accepts({ "GRT:R(Read)" })
void packageAdmin_withoutAssumedRole_canReadItsOwnGrantById() {
// given
final var givenCurrentUserAsPackageAdmin = new Subject("aaa00@aaa.example.com");
final var givenGranteeUser = findRbacUserByName("aaa00@aaa.example.com");
final var givenGrantedRole = findRbacRoleByName("package#aaa00.admin");
// when
final var grant = givenCurrentUserAsPackageAdmin.getGrantById()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(200)
.body("grantedByRoleIdName", is("customer#aaa.admin"))
.body("grantedRoleIdName", is("package#aaa00.admin"))
.body("granteeUserName", is("aaa00@aaa.example.com"));
}
@Test
@Accepts({ "GRT:R(Read)" })
void packageAdmin_withAssumedUnixUserAdmin_canNotReadItsOwnGrantById() {
// given
final var givenCurrentUserAsPackageAdmin = new Subject("aaa00@aaa.example.com", "unixuser#aaa00-aaaa.admin");
final var givenGranteeUser = findRbacUserByName("aaa00@aaa.example.com");
final var givenGrantedRole = findRbacRoleByName("package#aaa00.admin");
// when
final var grant = givenCurrentUserAsPackageAdmin.getGrantById()
.forGrantedRole(givenGrantedRole).toGranteeUser(givenGranteeUser);
// then
grant.assertThat()
.statusCode(404);
}
}
@Nested @Nested
class GrantRoleToUser { class GrantRoleToUser {
@ -166,6 +228,10 @@ class RbacGrantControllerAcceptanceTest {
this.assumedRole = assumedRole; this.assumedRole = assumedRole;
} }
public Subject(final String currentUser) {
this(currentUser, "");
}
GrantFixture grantsRole(final RbacRoleEntity givenOwnPackageAdminRole) { GrantFixture grantsRole(final RbacRoleEntity givenOwnPackageAdminRole) {
return new GrantFixture(givenOwnPackageAdminRole); return new GrantFixture(givenOwnPackageAdminRole);
} }
@ -174,6 +240,10 @@ class RbacGrantControllerAcceptanceTest {
return new RevokeFixture(givenOwnPackageAdminRole); return new RevokeFixture(givenOwnPackageAdminRole);
} }
GetGrantByIdFixture getGrantById() {
return new GetGrantByIdFixture();
}
class GrantFixture { class GrantFixture {
private Subject grantingSubject = Subject.this; private Subject grantingSubject = Subject.this;
@ -252,6 +322,34 @@ class RbacGrantControllerAcceptanceTest {
.then(); // @formatter:on .then(); // @formatter:on
} }
} }
private class GetGrantByIdFixture {
private Subject currentSubject = Subject.this;
private RbacRoleEntity grantedRole;
private boolean assumed;
private RbacUserEntity granteeUser;
GetGrantByIdFixture forGrantedRole(final RbacRoleEntity grantedRole) {
this.grantedRole = grantedRole;
return this;
}
ValidatableResponse toGranteeUser(final RbacUserEntity granteeUser) {
this.granteeUser = granteeUser;
return RestAssured // @formatter:ff
.given()
.header("current-user", currentSubject.currentUser)
.header("assumed-roles", currentSubject.assumedRole)
.port(port)
.when()
.get("http://localhost/api/rbac-grants/%s/%s".formatted(
grantedRole.getUuid(), granteeUser.getUuid()
))
.then(); // @formatter:on
}
}
} }
private void assumeGrantExists(final Subject grantingSubject, final String expectedGrant) { private void assumeGrantExists(final Subject grantingSubject, final String expectedGrant) {
@ -275,6 +373,13 @@ class RbacGrantControllerAcceptanceTest {
).returnedValue(); ).returnedValue();
} }
RbacUserEntity findRbacUserByName(final String userName) {
return jpaAttempt.transacted(() -> {
context.setCurrentUser("mike@hostsharing.net");
return rbacUserRepository.findByName(userName);
}).returnedValue();
}
RbacRoleEntity findRbacRoleByName(final String roleName) { RbacRoleEntity findRbacRoleByName(final String roleName) {
return jpaAttempt.transacted(() -> { return jpaAttempt.transacted(() -> {
context.setCurrentUser("mike@hostsharing.net"); context.setCurrentUser("mike@hostsharing.net");

View File

@ -108,7 +108,7 @@ class RbacGrantRepositoryIntegrationTest {
// given // given
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
assumedRoles("customer#aaa.admin"); assumedRoles("customer#aaa.admin");
final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName("aac00@aac.example.com"); final var givenArbitraryUserUuid = rbacUserRepository.findByName("aac00@aac.example.com").getUuid();
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid(); final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName("package#aaa00.admin").getUuid();
// when // when
@ -132,9 +132,7 @@ class RbacGrantRepositoryIntegrationTest {
@Transactional(propagation = Propagation.NEVER) @Transactional(propagation = Propagation.NEVER)
public void packageAdmin_canNotGrantPackageOwnerRole() { public void packageAdmin_canNotGrantPackageOwnerRole() {
// given // given
record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) { record Given(RbacUserEntity arbitraryUser, UUID packageOwnerRoleUuid) {}
}
final var given = jpaAttempt.transacted(() -> { final var given = jpaAttempt.transacted(() -> {
// to find the uuids of we need to have access rights to these // to find the uuids of we need to have access rights to these
currentUser("admin@aaa.example.com"); currentUser("admin@aaa.example.com");
@ -247,7 +245,7 @@ class RbacGrantRepositoryIntegrationTest {
private RbacGrantEntity create(GrantBuilder with) { private RbacGrantEntity create(GrantBuilder with) {
currentUser(with.byUserName); currentUser(with.byUserName);
assumedRoles(with.assumedRole); assumedRoles(with.assumedRole);
final var givenArbitraryUserUuid = rbacUserRepository.findUuidByName(with.granteeUserName); final var givenArbitraryUserUuid = rbacUserRepository.findByName(with.granteeUserName).getUuid();
final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName(with.grantedRole).getUuid(); final var givenOwnPackageRoleUuid = rbacRoleRepository.findByRoleName(with.grantedRole).getUuid();
final var grant = RbacGrantEntity.builder() final var grant = RbacGrantEntity.builder()