optionally limit account-context to natural persons (#187)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/187 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		| @@ -50,6 +50,9 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity< | |||||||
|     @Column(name = "qualifier", length = 80) |     @Column(name = "qualifier", length = 80) | ||||||
|     private String qualifier; |     private String qualifier; | ||||||
|  |  | ||||||
|  |     @Column(name = "only_for_natural_persons") | ||||||
|  |     private boolean onlyForNaturalPersons; | ||||||
|  |  | ||||||
|     @Override |     @Override | ||||||
|     public String toShortString() { |     public String toShortString() { | ||||||
|         return toString(); |         return toString(); | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatc | |||||||
| import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource; | import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource; | ||||||
| import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource; | import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource; | ||||||
| import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository; | import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository; | ||||||
|  | import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType; | ||||||
| import net.hostsharing.hsadminng.mapper.StrictMapper; | import net.hostsharing.hsadminng.mapper.StrictMapper; | ||||||
| import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | import net.hostsharing.hsadminng.persistence.EntityManagerWrapper; | ||||||
| import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; | import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; | ||||||
| @@ -193,9 +194,18 @@ public class HsCredentialsController implements CredentialsApi { | |||||||
|                     mapper.map(person, HsOfficePersonResource.class) |                     mapper.map(person, HsOfficePersonResource.class) | ||||||
|             ) |             ) | ||||||
|         ); |         ); | ||||||
|         resource.setContexts(mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class)); |  | ||||||
|  |         resource.setContexts(mapToValidContextResources(entity)); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     private List<ContextResource> mapToValidContextResources(final HsCredentialsEntity entity) { | ||||||
|  |         var allContexts = mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class); | ||||||
|  |         return allContexts.stream() | ||||||
|  |             .filter(context -> !context.getOnlyForNaturalPersons() || | ||||||
|  |                           entity.getPerson().getPersonType() == HsOfficePersonType.NATURAL_PERSON) | ||||||
|  |         .toList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { |     final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { | ||||||
|  |  | ||||||
|         // TODO.impl: we need to make sure that the current subject is OWNER (or ADMIN?) of the person |         // TODO.impl: we need to make sure that the current subject is OWNER (or ADMIN?) of the person | ||||||
|   | |||||||
| @@ -15,6 +15,8 @@ components: | |||||||
|                 qualifier: |                 qualifier: | ||||||
|                     type: string |                     type: string | ||||||
|                     maxLength: 80 |                     maxLength: 80 | ||||||
|  |                 onlyForNaturalPersons: | ||||||
|  |                     type: boolean | ||||||
|             required: |             required: | ||||||
|                 - uuid |                 - uuid | ||||||
|                 - type |                 - type | ||||||
|   | |||||||
| @@ -38,6 +38,8 @@ create table hs_accounts.context | |||||||
|     type                        varchar(16), |     type                        varchar(16), | ||||||
|     qualifier                   varchar(80), |     qualifier                   varchar(80), | ||||||
|  |  | ||||||
|  |     only_for_natural_persons    boolean default false, | ||||||
|  |  | ||||||
|     unique (type, qualifier) |     unique (type, qualifier) | ||||||
| ); | ); | ||||||
| --// | --// | ||||||
|   | |||||||
| @@ -26,15 +26,17 @@ begin | |||||||
|     personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran'); |     personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran'); | ||||||
|  |  | ||||||
|     -- Add test contexts |     -- Add test contexts | ||||||
|     INSERT INTO hs_accounts.context (uuid, type, qualifier) VALUES |     INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES | ||||||
|         ('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod') |         ('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true) | ||||||
|         RETURNING * INTO context_HSADMIN_prod; |         RETURNING * INTO context_HSADMIN_prod; | ||||||
|     INSERT INTO hs_accounts.context (uuid, type, qualifier) VALUES |     INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES | ||||||
|         ('22222222-2222-2222-2222-222222222222', 'SSH', 'internal') |         ('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true) | ||||||
|            RETURNING * INTO context_SSH_internal; |            RETURNING * INTO context_SSH_internal; | ||||||
|     INSERT INTO hs_accounts.context (uuid, type, qualifier) VALUES |     INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES | ||||||
|         ('33333333-3333-3333-3333-333333333333', 'MATRIX', 'internal') |         ('33333333-3333-3333-3333-333333333333', 'MATRIX', 'internal', true) | ||||||
|            RETURNING * INTO context_MATRIX_internal; |            RETURNING * INTO context_MATRIX_internal; | ||||||
|  |     INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons) VALUES | ||||||
|  |         ('44444444-4444-4444-4444-444444444444', 'MASTODON', 'external', false); | ||||||
|  |  | ||||||
| -- grant general access to public credential contexts | -- grant general access to public credential contexts | ||||||
| -- TODO_impl: RBAC rules for _rv do not yet work properly | -- TODO_impl: RBAC rules for _rv do not yet work properly | ||||||
|   | |||||||
| @@ -60,7 +60,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest | |||||||
|         final var rowsAfter = query.getResultList(); |         final var rowsAfter = query.getResultList(); | ||||||
|  |  | ||||||
|         // then |         // then | ||||||
|         assertThat(rowsAfter).as("hs_accounts.context_hv should now contain the test-data rows for the current timestamp").hasSize(3); |         assertThat(rowsAfter).as("hs_accounts.context_hv should now contain the test-data rows for the current timestamp").hasSize(4); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|   | |||||||
| @@ -27,8 +27,10 @@ import java.time.LocalDateTime; | |||||||
| import java.time.ZonedDateTime; | import java.time.ZonedDateTime; | ||||||
| import java.time.format.DateTimeFormatter; | import java.time.format.DateTimeFormatter; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  | import java.util.Set; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  |  | ||||||
|  | import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; | ||||||
| import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; | import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; | ||||||
| import static org.mockito.ArgumentMatchers.any; | import static org.mockito.ArgumentMatchers.any; | ||||||
| import static org.mockito.Mockito.when; | import static org.mockito.Mockito.when; | ||||||
| @@ -77,6 +79,49 @@ class HsCredentialsControllerRestTest { | |||||||
|     @MockitoBean |     @MockitoBean | ||||||
|     CredentialContextResourceToEntityMapper contextMapper; |     CredentialContextResourceToEntityMapper contextMapper; | ||||||
|  |  | ||||||
|  |     @Test | ||||||
|  |     void shouldFilterInvalidContextsRegardingNonNaturalPerson() throws Exception { | ||||||
|  |         // given | ||||||
|  |         final var givenCredentialsUuid = UUID.randomUUID(); | ||||||
|  |         final var contextForNP = HsCredentialsContextRealEntity.builder() | ||||||
|  |                 .uuid(UUID.randomUUID()) | ||||||
|  |                 .type("HSADMIN") | ||||||
|  |                 .qualifier("prod") | ||||||
|  |                 .onlyForNaturalPersons(true) | ||||||
|  |                 .build(); | ||||||
|  |         final var contextForAll = HsCredentialsContextRealEntity.builder() | ||||||
|  |                 .uuid(UUID.randomUUID()) | ||||||
|  |                 .type("SSH") | ||||||
|  |                 .qualifier("prod") | ||||||
|  |                 .onlyForNaturalPersons(false) | ||||||
|  |                 .build(); | ||||||
|  |         final var credentialsEntity = HsCredentialsEntity.builder() | ||||||
|  |                 .uuid(givenCredentialsUuid) | ||||||
|  |                 .person(HsOfficePersonRbacEntity.builder() | ||||||
|  |                         .uuid(PERSON_UUID) | ||||||
|  |                         .personType(LEGAL_PERSON) | ||||||
|  |                         .build()) | ||||||
|  |                 .subject(RbacSubjectEntity.builder().name("some-nickname").build()) | ||||||
|  |                 .loginContexts(Set.of(contextForNP, contextForAll)) | ||||||
|  |                 .build(); | ||||||
|  |         when(credentialsRepo.findByUuid(givenCredentialsUuid)) | ||||||
|  |                 .thenReturn(Optional.of(credentialsEntity)); | ||||||
|  |  | ||||||
|  |         // when | ||||||
|  |         mockMvc.perform(MockMvcRequestBuilders | ||||||
|  |                         .get("/api/hs/accounts/credentials/" + givenCredentialsUuid) | ||||||
|  |                         .header("Authorization", "Bearer test") | ||||||
|  |                         .accept(MediaType.APPLICATION_JSON)) | ||||||
|  |                 .andDo(print()) | ||||||
|  |  | ||||||
|  |                 // then | ||||||
|  |                 .andExpect(status().isOk()) | ||||||
|  |                 .andExpect(jsonPath("$.contexts.length()").value(1)) | ||||||
|  |                 .andExpect(jsonPath("$.contexts[0].type").value("SSH")) | ||||||
|  |                 .andExpect(jsonPath("$.contexts[0].qualifier").value("prod")) | ||||||
|  |                 .andExpect(jsonPath("$.contexts[0].onlyForNaturalPersons").value(false)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     @Test |     @Test | ||||||
|     void patchCredentialsUsed() throws Exception { |     void patchCredentialsUsed() throws Exception { | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user