diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java index c497019e..d0dc4d93 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleEntity.java @@ -1,9 +1,6 @@ package net.hostsharing.hsadminng.rbac.rbacrole; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hibernate.annotations.Formula; import org.springframework.data.annotation.Immutable; @@ -14,6 +11,7 @@ import java.util.UUID; @Table(name = "rbacrole_rv") @Getter @Setter +@ToString @Immutable @NoArgsConstructor @AllArgsConstructor @@ -25,16 +23,16 @@ public class RbacRoleEntity { @Column(name="objectuuid") private UUID objectUuid; - @Column(name="roletype") - @Enumerated(EnumType.STRING) - private RbacRoleType roleType; - @Column(name="objecttable") private String objectTable; @Column(name="objectidname") private String objectIdName; + @Column(name="roletype") + @Enumerated(EnumType.STRING) + private RbacRoleType roleType; + @Formula("objectTable||'#'||objectIdName||'.'||roleType") private String roleName; } diff --git a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepository.java b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepository.java index 1df718fb..0af5091c 100644 --- a/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepository.java +++ b/src/main/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepository.java @@ -1,58 +1,17 @@ package net.hostsharing.hsadminng.rbac.rbacrole; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.data.repository.Repository; import java.util.List; -import java.util.Optional; import java.util.UUID; public interface RbacRoleRepository extends Repository { - /** - * Retrieves an entity by its id. - * - * @param id must not be {@literal null}. - * @return the entity with the given id or {@literal Optional#empty()} if none found. - * @throws IllegalArgumentException if {@literal id} is {@literal null}. - */ - Optional findByUuid(UUID id); - - /** - * Returns whether an entity with the given id exists. - * - * @param id must not be {@literal null}. - * @return {@literal true} if an entity with the given id exists, {@literal false} otherwise. - * @throws IllegalArgumentException if {@literal id} is {@literal null}. - */ - boolean existsByUuid(RbacRoleEntity id); - /** * Returns all instances of the type. * * @return all entities */ - Iterable findAll(); - - /** - * Returns all entities sorted by the given options. - * - * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be - * {@literal null}. - * @return all entities sorted by the given options - */ - List findAll(Sort sort); - - /** - * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object. - * - * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be - * {@literal null}. - * @return a page of entities - */ - Page findAll(Pageable pageable); - + List findAll(); } diff --git a/src/main/resources/db/changelog/2022-07-28-020-rbac-role-builder.sql b/src/main/resources/db/changelog/2022-07-28-020-rbac-role-builder.sql index 35ec5ea8..255b6ccb 100644 --- a/src/main/resources/db/changelog/2022-07-28-020-rbac-role-builder.sql +++ b/src/main/resources/db/changelog/2022-07-28-020-rbac-role-builder.sql @@ -179,7 +179,6 @@ declare userUuid uuid; begin raise notice 'will createRole for %', roleDescriptor; - raise notice 'will createRole for % % %', roleDescriptor.objecttable, roleDescriptor.objectuuid, roleDescriptor.roletype; roleUuid = createRole(roleDescriptor); call grantPermissionsToRole(roleUuid, permissions.permissionUuids); diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql index f5b8263d..d7588ec4 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package-rbac.sql @@ -24,7 +24,7 @@ create or replace function packageOwner(pac package) returns null on null input language plpgsql as $$ begin - return roleDescriptor('package', pac.uuid, 'admin'); + return roleDescriptor('package', pac.uuid, 'owner'); end; $$; create or replace function packageAdmin(pac package) diff --git a/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql b/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql index 6e08b1e0..78274594 100644 --- a/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql +++ b/src/main/resources/db/changelog/2022-07-29-070-hs-package-test-data.sql @@ -23,7 +23,7 @@ create or replace procedure createPackageTestData( loop CONTINUE WHEN cust.reference < minCustomerReference; - for t in 0..randominrange(1, 2) + for t in 0..2 loop pacName = cust.prefix || to_char(t, 'fm00'); currentTask = 'creating RBAC test package #' || pacName || ' for customer ' || cust.prefix || ' #' || @@ -59,4 +59,3 @@ do language plpgsql $$ end; $$; --// - diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java new file mode 100644 index 00000000..f80599d3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleControllerRestTest.java @@ -0,0 +1,54 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import net.hostsharing.hsadminng.context.Context; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static java.util.Arrays.asList; +import static net.hostsharing.hsadminng.rbac.rbacrole.TestRbacRole.*; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(RbacRoleController.class) +class RbacRoleControllerRestTest { + + @Autowired + MockMvc mockMvc; + @MockBean + Context contextMock; + @MockBean + RbacRoleRepository rbacRoleRepository; + + @Test + void apiCustomersWillReturnCustomersFromRepository() throws Exception { + + // given + when(rbacRoleRepository.findAll()).thenReturn( + asList(hostmasterRole, customerXxxOwner, customerXxxAdmin)); + + // when + mockMvc.perform(MockMvcRequestBuilders + .get("/api/rbacroles") + .header("current-user", "mike@hostsharing.net") + .accept(MediaType.APPLICATION_JSON)) + + // then + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[0].roleName", is("global#hostsharing.admin"))) + .andExpect(jsonPath("$[1].roleName", is("customer#xxx.owner"))) + .andExpect(jsonPath("$[2].roleName", is("customer#xxx.admin"))) + .andExpect(jsonPath("$[2].uuid", is(customerXxxAdmin.getUuid().toString()))) + .andExpect(jsonPath("$[2].objectUuid", is(customerXxxAdmin.getObjectUuid().toString()))) + .andExpect(jsonPath("$[2].objectTable", is(customerXxxAdmin.getObjectTable().toString()))) + .andExpect(jsonPath("$[2].objectIdName", is(customerXxxAdmin.getObjectIdName().toString()))); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java new file mode 100644 index 00000000..5542bb9f --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/RbacRoleRepositoryIntegrationTest.java @@ -0,0 +1,169 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import net.hostsharing.hsadminng.context.Context; +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 javax.persistence.EntityManager; +import javax.transaction.Transactional; + +import static net.hostsharing.test.JpaAttempt.attempt; +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ComponentScan(basePackageClasses = { Context.class, RbacRoleRepository.class }) +class RbacRoleRepositoryIntegrationTest { + + @Autowired + Context context; + + @Autowired + RbacRoleRepository rbacRoleRepository; + + @Autowired EntityManager em; + + @Nested + class FindAllRbacRoles { + + private static final String[] ALL_TEST_DATA_ROLES = new String[] { + // @formatter:off + "global#hostsharing.admin", + "customer#aaa.admin", "customer#aaa.owner", "customer#aaa.tenant", + "package#aaa00.admin", "package#aaa00.owner", "package#aaa00.tenant", + "package#aaa01.admin", "package#aaa01.owner", "package#aaa01.tenant", + "package#aaa02.admin", "package#aaa02.owner", "package#aaa02.tenant", + "customer#aab.admin", "customer#aab.owner", "customer#aab.tenant", + "package#aab00.admin", "package#aab00.owner", "package#aab00.tenant", + "package#aab01.admin", "package#aab01.owner", "package#aab01.tenant", + "package#aab02.admin", "package#aab02.owner", "package#aab02.tenant", + "customer#aac.admin", "customer#aac.owner", "customer#aac.tenant", + "package#aac00.admin", "package#aac00.owner", "package#aac00.tenant", + "package#aac01.admin", "package#aac01.owner", "package#aac01.tenant", + "package#aac02.admin", "package#aac02.owner", "package#aac02.tenant" + // @formatter:on + }; + + @Test + public void hostsharingAdmin_withoutAssumedRole_canViewAllRbacRoles() { + // given + currentUser("mike@hostsharing.net"); + + // when + final var result = rbacRoleRepository.findAll(); + + // then + exactlyTheseRbacRolesAreReturned(result, ALL_TEST_DATA_ROLES); + } + + @Test + public void hostsharingAdmin_withAssumedHostsharingAdminRole_canViewAllRbacRoles() { + given: + currentUser("mike@hostsharing.net"); + assumedRoles("global#hostsharing.admin"); + + // when + final var result = rbacRoleRepository.findAll(); + + then: + exactlyTheseRbacRolesAreReturned(result, ALL_TEST_DATA_ROLES); + } + + @Test + public void RbacRoleAdmin_withoutAssumedRole_canViewOnlyItsOwnRbacRole() { + // given: + currentUser("admin@aaa.example.com"); + + // when: + final var result = rbacRoleRepository.findAll(); + + // then: + exactlyTheseRbacRolesAreReturned( + result, + // @formatter:off + "customer#aaa.admin", "customer#aaa.tenant", + "package#aaa00.admin", "package#aaa00.owner", "package#aaa00.tenant", + "package#aaa01.admin", "package#aaa01.owner", "package#aaa01.tenant", + "package#aaa02.admin", "package#aaa02.owner", "package#aaa02.tenant" + // @formatter:on + ); + } + + @Test + public void RbacRoleAdmin_withAssumedOwnedPackageAdminRole_canViewOnlyItsOwnRbacRole() { + currentUser("admin@aaa.example.com"); + assumedRoles("package#aaa00.admin"); + + final var result = rbacRoleRepository.findAll(); + + exactlyTheseRbacRolesAreReturned(result, "customer#aaa.tenant", "package#aaa00.tenant", "package#aaa00.admin"); + } + + @Test + public void RbacRoleAdmin_withAssumedAlienPackageAdminRole_cannotViewAnyRbacRole() { + // given: + currentUser("admin@aaa.example.com"); + assumedRoles("package#aab00.admin"); + + // when + final var attempt = attempt( + em, + () -> rbacRoleRepository.findAll()); + + // then + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "user admin@aaa.example.com .* has no permission to assume role package#aab00#admin"); + } + + @Test + void unknownUser_withoutAssumedRole_cannotViewAnyRbacRoles() { + currentUser("unknown@example.org"); + + final var attempt = attempt( + em, + () -> rbacRoleRepository.findAll()); + + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); + } + + @Test + @Transactional + void unknownUser_withAssumedRbacRoleRole_cannotViewAnyRbacRoles() { + currentUser("unknown@example.org"); + assumedRoles("RbacRole#aaa.admin"); + + final var attempt = attempt( + em, + () -> rbacRoleRepository.findAll()); + + attempt.assertExceptionWithRootCauseMessage( + JpaSystemException.class, + "hsadminng.currentUser defined as unknown@example.org, but does not exists"); + } + + } + + 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 exactlyTheseRbacRolesAreReturned(final Iterable actualResult, final String... rbacRoleNames) { + assertThat(actualResult) + //.hasSize(rbacRoleNames.length) + .extracting(RbacRoleEntity::getRoleName) + .containsExactlyInAnyOrder(rbacRoleNames); + } + +} diff --git a/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java new file mode 100644 index 00000000..cabb96b3 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/rbac/rbacrole/TestRbacRole.java @@ -0,0 +1,14 @@ +package net.hostsharing.hsadminng.rbac.rbacrole; + +import static java.util.UUID.randomUUID; + +public class TestRbacRole { + + public static final RbacRoleEntity hostmasterRole = rbacRole("global", "hostsharing", RbacRoleType.admin); + static final RbacRoleEntity customerXxxOwner = rbacRole("customer", "xxx", RbacRoleType.owner); + static final RbacRoleEntity customerXxxAdmin = rbacRole("customer", "xxx", RbacRoleType.admin); + + static public RbacRoleEntity rbacRole(final String objectTable, final String objectIdName, final RbacRoleType roleType) { + return new RbacRoleEntity(randomUUID(), randomUUID(), objectTable, objectIdName, roleType, objectTable+'#'+objectIdName+'.'+roleType); + } +}