From bae13d55031000505c3a8645b9c5687bf22a7788 Mon Sep 17 00:00:00 2001 From: Michael Hoennig Date: Fri, 12 Sep 2025 11:37:55 +0200 Subject: [PATCH] rename Credentials->Profile + Context->Scope (#202) Co-authored-by: Michael Hoennig Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/202 Reviewed-by: Timotheus Pokorra --- doc/ideas/account-profiles-data-model.mermaid | 49 ++++ .../login-credentials-data-model.mermaid | 49 ---- .../HsCredentialsContextRbacRepository.java | 24 -- .../HsCredentialsContextRealRepository.java | 24 -- .../hs/accounts/HsCredentialsRepository.java | 43 ---- ...ntroller.java => HsProfileController.java} | 213 ++++++++---------- ...ntialsEntity.java => HsProfileEntity.java} | 41 ++-- ...tcher.java => HsProfileEntityPatcher.java} | 18 +- .../hs/accounts/HsProfileRepository.java | 43 ++++ ...ntialsContext.java => HsProfileScope.java} | 16 +- ...ler.java => HsProfileScopeController.java} | 18 +- ...ity.java => HsProfileScopeRbacEntity.java} | 8 +- .../HsProfileScopeRbacRepository.java | 24 ++ ...ity.java => HsProfileScopeRealEntity.java} | 4 +- .../HsProfileScopeRealRepository.java | 24 ++ ....java => ScopeResourceToEntityMapper.java} | 40 ++-- .../hs/validation/ValidatableProperty.java | 2 +- .../api-definition/accounts/api-mappings.yaml | 2 +- .../api-definition/accounts/api-paths.yaml | 19 +- .../accounts/credentials-with-uuid-used.yaml | 23 -- .../api-definition/accounts/current.yaml | 4 +- ...ials-schemas.yaml => profile-schemas.yaml} | 37 ++- ...-with-uuid.yaml => profile-with-uuid.yaml} | 34 +-- .../{credentials.yaml => profiles.yaml} | 22 +- ...ontext-schemas.yaml => scope-schemas.yaml} | 2 +- .../accounts/{contexts.yaml => scopes.yaml} | 10 +- src/main/resources/application.yml | 2 +- .../db/changelog/1-rbac/1050-rbac-base.sql | 37 +-- ...s-office-person-test-data-for-accounts.sql | 10 +- .../5028-hs-office-person-test-data.sql | 10 +- .../950-accounts/9500-hs-accounts-schema.sql | 2 +- .../950-accounts/9510-hs-accounts.sql | 42 ++-- .../9513-hs-accounts-context-rbac.md | 41 ---- .../9513-hs-accounts-scope-rbac.md | 41 ++++ .../9519-hs-accounts-test-data.sql | 74 +++--- .../resources/i18n/messages_de.properties | 16 +- .../resources/i18n/messages_en.properties | 16 +- .../resources/i18n/messages_fr.properties | 16 +- .../HsCredentialsEntityPatcherUnitTest.java | 168 -------------- ...=> HsProfileControllerAcceptanceTest.java} | 193 +++++++--------- .../HsProfileEntityPatcherUnitTest.java | 167 ++++++++++++++ ...> HsProfileRepositoryIntegrationTest.java} | 102 ++++----- ... => HsProfileScopeControllerRestTest.java} | 38 ++-- ... => HsProfileScopeRbacEntityUnitTest.java} | 8 +- ...leScopeRbacRepositoryIntegrationTest.java} | 54 ++--- ... => HsProfileScopeRealEntityUnitTest.java} | 8 +- ...leScopeRealRepositoryIntegrationTest.java} | 56 ++--- .../scenarios/BaseCredentialsUseCase.java | 38 ---- .../scenarios/BaseProfileUseCase.java | 38 ++++ ...ateCredentials.java => CreateProfile.java} | 28 ++- ...ioTests.java => ProfileScenarioTests.java} | 29 ++- ...ateCredentials.java => UpdateProfile.java} | 24 +- ...ceBankAccountControllerAcceptanceTest.java | 5 +- 53 files changed, 976 insertions(+), 1080 deletions(-) create mode 100644 doc/ideas/account-profiles-data-model.mermaid delete mode 100644 doc/ideas/login-credentials-data-model.mermaid delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepository.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepository.java delete mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepository.java rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsController.java => HsProfileController.java} (51%) rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsEntity.java => HsProfileEntity.java} (64%) rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsEntityPatcher.java => HsProfileEntityPatcher.java} (57%) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileRepository.java rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContext.java => HsProfileScope.java} (79%) rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextsController.java => HsProfileScopeController.java} (65%) rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRbacEntity.java => HsProfileScopeRbacEntity.java} (88%) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepository.java rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRealEntity.java => HsProfileScopeRealEntity.java} (81%) create mode 100644 src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepository.java rename src/main/java/net/hostsharing/hsadminng/hs/accounts/{CredentialContextResourceToEntityMapper.java => ScopeResourceToEntityMapper.java} (52%) delete mode 100644 src/main/resources/api-definition/accounts/credentials-with-uuid-used.yaml rename src/main/resources/api-definition/accounts/{credentials-schemas.yaml => profile-schemas.yaml} (82%) rename src/main/resources/api-definition/accounts/{credentials-with-uuid.yaml => profile-with-uuid.yaml} (60%) rename src/main/resources/api-definition/accounts/{credentials.yaml => profiles.yaml} (62%) rename src/main/resources/api-definition/accounts/{context-schemas.yaml => scope-schemas.yaml} (96%) rename src/main/resources/api-definition/accounts/{contexts.yaml => scopes.yaml} (60%) delete mode 100644 src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-context-rbac.md create mode 100644 src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-scope-rbac.md delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcherUnitTest.java rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsControllerAcceptanceTest.java => HsProfileControllerAcceptanceTest.java} (65%) create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcherUnitTest.java rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsRepositoryIntegrationTest.java => HsProfileRepositoryIntegrationTest.java} (77%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextsControllerRestTest.java => HsProfileScopeControllerRestTest.java} (86%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRbacEntityUnitTest.java => HsProfileScopeRbacEntityUnitTest.java} (73%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRbacRepositoryIntegrationTest.java => HsProfileScopeRbacRepositoryIntegrationTest.java} (70%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRealEntityUnitTest.java => HsProfileScopeRealEntityUnitTest.java} (73%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/{HsCredentialsContextRealRepositoryIntegrationTest.java => HsProfileScopeRealRepositoryIntegrationTest.java} (69%) delete mode 100644 src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseCredentialsUseCase.java create mode 100644 src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseProfileUseCase.java rename src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/{CreateCredentials.java => CreateProfile.java} (64%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/{CredentialsScenarioTests.java => ProfileScenarioTests.java} (80%) rename src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/{UpdateCredentials.java => UpdateProfile.java} (60%) diff --git a/doc/ideas/account-profiles-data-model.mermaid b/doc/ideas/account-profiles-data-model.mermaid new file mode 100644 index 00000000..22154c44 --- /dev/null +++ b/doc/ideas/account-profiles-data-model.mermaid @@ -0,0 +1,49 @@ +classDiagram + direction LR + +OfficePerson "1" o.. "*" Profile +Profile "1" o-- "1" RbacSubject + +Scope "1..n" --o "1" ScopeMapping +Profile "1..n" --o "1" ScopeMapping + +class Profile{ + +emailAdress: text + +smsNumber: text + +password: text + +totpSecrets: text + +phonePassword: text + -active: bool [r/w] + -globalUid: int [w/o] + -globalGid: int [w/o] +} + +class Scope{ + -type: Enum [SSH, Matrix, Mastodon, ...] + -qualifier: text +} + +class ScopeMapping{ + note for ScopeMapping "Assigns Profile to Scopes" +} + +class RbacSubject{ + +uuid: uuid + +name: text # == nickname +} + +class OfficePerson{ + +type: enum + +tradename: text + +title: text + +familyName: text + +givenName: text + +salutation: text +} + +style Scope fill:#00f,color:#fff +style ScopeMapping fill:#00f,color:#fff +style Profile fill:#00f,color:#fff + +style RbacSubject fill:#f96,color:#fff +style OfficePerson fill:#f66,color:#000 diff --git a/doc/ideas/login-credentials-data-model.mermaid b/doc/ideas/login-credentials-data-model.mermaid deleted file mode 100644 index 9bcf775a..00000000 --- a/doc/ideas/login-credentials-data-model.mermaid +++ /dev/null @@ -1,49 +0,0 @@ -classDiagram - direction LR - - OfficePerson o.. "*" Credentials - Credentials "1" o-- "1" RbacSubject - - CredentialsContext "1..n" --o "1" CredentialsContextMapping - Credentials "1..n" --o "1" CredentialsContextMapping - - class Credentials{ - +totpSecret: text - +phonePassword: text - +emailAdress: text - +smsNumber: text - -active: bool [r/w] - -globalUid: int [w/o] - -globalGid: int [w/o] - -onboardingToken: text [w/o] - } - - class CredentialsContext{ - -type: Enum [SSH, Matrix, Mastodon, ...] - -qualifier: text - } - - class CredentialsContextMapping{ - } - note for CredentialsContextMapping "Assigns Credentials to CredentialsContexts" - - class RbacSubject{ - +uuid: uuid - +name: text # == nickname - } - - class OfficePerson{ - +type: enum - +tradename: text - +title: text - +familyName: text - +givenName: text - +salutation: text - } - - style CredentialsContext fill:#00f,color:#fff - style CredentialsContextMapping fill:#00f,color:#fff - style Credentials fill:#00f,color:#fff - - style RbacSubject fill:#f96,color:#fff - style OfficePerson fill:#f66,color:#000 diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepository.java deleted file mode 100644 index c8844cd2..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.hostsharing.hsadminng.hs.accounts; - -import io.micrometer.core.annotation.Timed; -import org.springframework.data.repository.Repository; - -import jakarta.validation.constraints.NotNull; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public interface HsCredentialsContextRbacRepository extends Repository { - - @Timed("app.login.context.repo.findAll") - List findAll(); - - @Timed("app.login.context.repo.findByUuid") - Optional findByUuid(final UUID id); - - @Timed("app.login.context.repo.findByTypeAndQualifier") - Optional findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier); - - @Timed("app.login.context.repo.save") - HsCredentialsContextRbacEntity save(final HsCredentialsContextRbacEntity entity); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepository.java deleted file mode 100644 index c335da9e..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.hostsharing.hsadminng.hs.accounts; - -import io.micrometer.core.annotation.Timed; -import org.springframework.data.repository.Repository; - -import jakarta.validation.constraints.NotNull; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public interface HsCredentialsContextRealRepository extends Repository { - - @Timed("app.login.context.repo.findAll") - List findAll(); - - @Timed("app.login.context.repo.findByUuid") - Optional findByUuid(final UUID id); - - @Timed("app.login.context.repo.findByTypeAndQualifier") - Optional findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier); - - @Timed("app.login.context.repo.save") - HsCredentialsContextRealEntity save(final HsCredentialsContextRealEntity entity); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepository.java deleted file mode 100644 index d24d5cd5..00000000 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package net.hostsharing.hsadminng.hs.accounts; - -import io.micrometer.core.annotation.Timed; -import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.Repository; - -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -public interface HsCredentialsRepository extends Repository { - - @Timed("app.login.credentials.repo.findByUuid") - Optional findByUuid(final UUID uuid); - - @Timed("app.login.credentials.repo.findByPerson") - List findByPerson(final HsOfficePerson personUuid); - - @Timed("app.login.credentials.repo.findByCurrentSubject") - @Query(nativeQuery = true, value = """ - WITH RECURSIVE - same_person AS ( - SELECT own_credentials.person_uuid - FROM hs_accounts.credentials own_credentials - WHERE own_credentials.uuid = rbac.currentSubjectUuid() - ), - represented_persons AS ( - SELECT relation.anchorUuid person_uuid - FROM hs_office.relation relation - WHERE relation.type = 'REPRESENTATIVE' - AND relation.holderUuid IN (SELECT person_uuid FROM same_person) - ) - SELECT DISTINCT credentials.* - FROM hs_accounts.credentials credentials - WHERE credentials.person_uuid IN (SELECT person_uuid FROM same_person) - OR credentials.person_uuid IN (SELECT person_uuid FROM represented_persons) - """) - List findByCurrentSubject(); - - @Timed("app.login.credentials.repo.save") - HsCredentialsEntity save(final HsCredentialsEntity entity); -} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileController.java similarity index 51% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileController.java index 15687dd0..8d61c84c 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileController.java @@ -1,7 +1,5 @@ package net.hostsharing.hsadminng.hs.accounts; -import java.time.LocalDateTime; -import java.time.ZoneOffset; import java.util.List; import java.util.UUID; import java.util.function.BiConsumer; @@ -10,15 +8,15 @@ import java.util.stream.Collectors; import io.micrometer.core.annotation.Timed; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.val; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource; import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CurrentLoginUserResource; import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource; import net.hostsharing.hsadminng.config.MessageTranslator; import net.hostsharing.hsadminng.rbac.context.Context; -import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsInsertResource; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ProfileApi; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileInsertResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileResource; import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource; import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository; @@ -37,13 +35,12 @@ import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBui import jakarta.persistence.EntityNotFoundException; import jakarta.validation.ValidationException; -import static java.util.Optional.ofNullable; import static java.util.Optional.of; @RestController @PreAuthorize("isAuthenticated()") @SecurityRequirement(name = "bearerAuth") -public class HsCredentialsController implements CredentialsApi { +public class HsProfileController implements ProfileApi { @Autowired private Context context; @@ -58,7 +55,7 @@ public class HsCredentialsController implements CredentialsApi { private RbacSubjectRepository subjectRepo; @Autowired - private CredentialContextResourceToEntityMapper contextMapper; + private ScopeResourceToEntityMapper scopeMapper; @Autowired private MessageTranslator messageTranslator; @@ -67,113 +64,113 @@ public class HsCredentialsController implements CredentialsApi { private HsOfficePersonRealRepository realPersonRepo; @Autowired - private HsCredentialsRepository credentialsRepo; + private HsProfileRepository profileRepo; @Autowired private RbacSubjectRepository rbacSubjectRepo; @Override @Transactional(readOnly = true) - @Timed("app.credentials.credentials.getSingleCredentialsByUuid") - public ResponseEntity getSingleCredentialsByUuid(final UUID credentialsUuid) { + @Timed("app.accounts.profile.getSingleProfileByUuid") + public ResponseEntity getSingleProfileByUuid(final UUID profileUuid) { context.define(); // without assumed roles, otherwise we cannot access the subject anymore - val credentialsEntity = credentialsRepo.findByUuid(credentialsUuid); - if (credentialsEntity.isEmpty()) { + val profileEntity = profileRepo.findByUuid(profileUuid); + if (profileEntity.isEmpty()) { return ResponseEntity.notFound().build(); } val result = mapper.map( - credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + profileEntity.get(), ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(result); } @Override @Transactional(readOnly = true) - @Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid") - public ResponseEntity> getListOfCredentials( + @Timed("app.accounts.profile.getListOfProfileByPersonUuid") + public ResponseEntity> getListOfProfile( final String assumedRoles, final UUID personUuid ) { context.assumeRoles(assumedRoles); - val credentials = personUuid == null - ? credentialsRepo.findByCurrentSubject() + val profile = personUuid == null + ? profileRepo.findByCurrentSubject() : findByPersonUuid(personUuid); val result = mapper.mapList( - credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + profile, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(result); } @Override @Transactional - @Timed("app.credentials.credentials.postNewCredentials") - public ResponseEntity postNewCredentials( - final CredentialsInsertResource body + @Timed("app.accounts.profile.postNewProfile") + public ResponseEntity postNewProfile( + final ProfileInsertResource body ) { context.define(); // without assumed roles, otherwise we cannot access the subject anymore // first create and save the subject to get its UUID val newlySavedSubject = createSubject(body.getNickname()); - // afterward, create and save the credentials entity with the subject's UUID - val newCredentialsEntity = mapper.map( - body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); - validateOnCreate(newCredentialsEntity); + // afterward, create and save the profile entity with the subject's UUID + val newProfileEntity = mapper.map( + body, HsProfileEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER); + validateOnCreate(newProfileEntity); // switch to the new subject to get access to its own subject RBAC object context.define("activate newly created self-service subject", null, body.getNickname(), null); - newCredentialsEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject - em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository! + newProfileEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject + em.persist(newProfileEntity); // newProfileEntity.uuid == newlySavedSubject.uuid => do not use repository! - // return the new credentials as a resource + // return the new profile as a resource val uri = MvcUriComponentsBuilder.fromController(getClass()) - .path("/api/hs/accounts/credentials/{id}") - .buildAndExpand(newCredentialsEntity.getUuid()) + .path("/api/hs/accounts/profiles/{id}") + .buildAndExpand(newProfileEntity.getUuid()) .toUri(); - val newCredentialsResource = mapper.map( - newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); - return ResponseEntity.created(uri).body(newCredentialsResource); + val newProfileResource = mapper.map( + newProfileEntity, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + return ResponseEntity.created(uri).body(newProfileResource); } @Override @Transactional - @Timed("app.credentials.credentials.deleteCredentialsByUuid") - public ResponseEntity deleteCredentialsByUuid(final UUID credentialsUuid) { + @Timed("app.accounts.profile.deleteProfileByUuid") + public ResponseEntity deleteProfileByUuid(final UUID profileUuid) { context.define(); // without assumed roles, otherwise we cannot access the subject anymore - val credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid); - credentialsEntity.getLoginContexts().clear(); - validateOnDelete(credentialsEntity); + val profileEntity = em.getReference(HsProfileEntity.class, profileUuid); + profileEntity.getScopes().clear(); + validateOnDelete(profileEntity); em.flush(); - em.remove(credentialsEntity); - em.remove(credentialsEntity.getSubject()); + em.remove(profileEntity); + em.remove(profileEntity.getSubject()); return ResponseEntity.noContent().build(); } @Override @Transactional - @Timed("app.credentials.credentials.patchCredentials") - public ResponseEntity patchCredentials( - final UUID credentialsUuid, - final CredentialsPatchResource body + @Timed("app.accounts.profile.patchProfile") + public ResponseEntity patchProfile( + final UUID profileUuid, + final ProfilePatchResource body ) { context.define(); // without assumed roles, otherwise we cannot access the subject anymore - val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow(); + val current = profileRepo.findByUuid(profileUuid).orElseThrow(); - new HsCredentialsEntityPatcher(contextMapper, current).apply(body); + new HsProfileEntityPatcher(scopeMapper, current).apply(body); validateOnUpdate(current); - val saved = credentialsRepo.save(current); + val saved = profileRepo.save(current); val mapped = mapper.map( - saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); + saved, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); return ResponseEntity.ok(mapped); } @Override @Transactional - @Timed("app.credentials.credentials.getCurrentLoginUser") + @Timed("app.accounts.profile.getCurrentLoginUser") public ResponseEntity getCurrentLoginUser() { // define a context without assumed roles, otherwise we cannot access the subject anymore @@ -182,7 +179,7 @@ public class HsCredentialsController implements CredentialsApi { // fetch the data val currentSubjectUuid = context.fetchCurrentSubjectUuid(); val currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid); - val person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson(); + val person = profileRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson(); final boolean isGlobalAdmin = context.isGlobalAdmin(); @@ -191,46 +188,30 @@ public class HsCredentialsController implements CredentialsApi { return ResponseEntity.ok(result); } - @Override - @Transactional - @Timed("app.credentials.credentials.credentialsUsed") - public ResponseEntity credentialsUsed(final UUID credentialsUuid) { - context.define(); - - val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow(); - - current.setOnboardingToken(null); - current.setLastUsed(LocalDateTime.now()); - - val saved = credentialsRepo.save(current); - val mapped = mapper.map(saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER); - return ResponseEntity.ok(mapped); + private void validateOnCreate(final HsProfileEntity newProfileEntity) { + validateReferencedPersonToBeRepresentedByLoginUserPerson(newProfileEntity); + validateNormalUsersOnlyAccessPublicScopes(newProfileEntity); + validateNaturalPersonRequirementOfScopes(newProfileEntity); } - private void validateOnCreate(final HsCredentialsEntity newCredentialsEntity) { - validateReferencedPersonToBeRepresentedByLoginUserPerson(newCredentialsEntity); - validateNormalUsersOnlyAccessPublicContexts(newCredentialsEntity); - validateNaturalPersonRequirementOfContexts(newCredentialsEntity); + private void validateOnUpdate(final HsProfileEntity current) { + validateNormalUsersOnlyAccessPublicScopes(current); + validateNaturalPersonRequirementOfScopes(current); + validateOwnHsadminProfileMustNotBeRemoved(current); } - private void validateOnUpdate(final HsCredentialsEntity current) { - validateNormalUsersOnlyAccessPublicContexts(current); - validateNaturalPersonRequirementOfContexts(current); - validateOwnHsadminCredentialsMustNotBeRemoved(current); + private void validateOnDelete(final HsProfileEntity profileEntity) { + validateOwnHsadminProfileMustNotBeRemoved(profileEntity); } - private void validateOnDelete(final HsCredentialsEntity credentialsEntity) { - validateOwnHsadminCredentialsMustNotBeRemoved(credentialsEntity); - } - - private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsCredentialsEntity newCredentialsEntity) { + private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsProfileEntity newProfileEntity) { if (context.isGlobalAdmin()) { return; } - val referredPersonUuid = newCredentialsEntity.getPerson().getUuid(); + val referredPersonUuid = newProfileEntity.getPerson().getUuid(); val currentSubjectUuid = context.fetchCurrentSubjectUuid(); - val loginPersonUuid = credentialsRepo.findByUuid(currentSubjectUuid) - .map(HsCredentialsEntity::getPerson) + val loginPersonUuid = profileRepo.findByUuid(currentSubjectUuid) + .map(HsProfileEntity::getPerson) .map(HsOfficePerson::getUuid) .orElseThrow(); val representedPersonUuids = realPersonRepo.findPersonsRepresentedByPersonWithUuid(loginPersonUuid) @@ -238,58 +219,58 @@ public class HsCredentialsController implements CredentialsApi { if ( !representedPersonUuids.contains(referredPersonUuid)) { throw new ValidationException( messageTranslator.translate( - "credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person", + "profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person", loginPersonUuid)); } } - private void validateNormalUsersOnlyAccessPublicContexts(final HsCredentialsEntity newCredentialsEntity) { - val forbiddenContexts = newCredentialsEntity.getLoginContexts().stream() + private void validateNormalUsersOnlyAccessPublicScopes(final HsProfileEntity newProfileEntity) { + val forbiddenScopes = newProfileEntity.getScopes().stream() .filter(c -> !c.isPublicAccess() && !context.isGlobalAdmin() ) .toList(); - if (!forbiddenContexts.isEmpty()) { + if (!forbiddenScopes.isEmpty()) { throw new ValidationException( messageTranslator.translate( - "credentials.access-denied-for-contexts-{0}", - toDisplay(forbiddenContexts) + "profile.access-denied-for-scopes-{0}", + toDisplay(forbiddenScopes) )); } } - private void validateNaturalPersonRequirementOfContexts(final HsCredentialsEntity newCredentialsEntity) { - if (newCredentialsEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) { + private void validateNaturalPersonRequirementOfScopes(final HsProfileEntity newProfileEntity) { + if (newProfileEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) { return; } - val contextsWhichRequireNaturalPerson = newCredentialsEntity.getLoginContexts().stream() - .filter(HsCredentialsContext::isOnlyForNaturalPersons) + val scopesWhichRequireNaturalPerson = newProfileEntity.getScopes().stream() + .filter(HsProfileScope::isOnlyForNaturalPersons) .toList(); - if (!contextsWhichRequireNaturalPerson.isEmpty()) { + if (!scopesWhichRequireNaturalPerson.isEmpty()) { throw new ValidationException( messageTranslator.translate( - "credentials.context-requires-natural-person-{0}", - toDisplay(contextsWhichRequireNaturalPerson) + "profile.scope-requires-natural-person-{0}", + toDisplay(scopesWhichRequireNaturalPerson) )); } } - private void validateOwnHsadminCredentialsMustNotBeRemoved(final HsCredentialsEntity newCredentialsEntity) { - if (!newCredentialsEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) { + private void validateOwnHsadminProfileMustNotBeRemoved(final HsProfileEntity newProfileEntity) { + if (!newProfileEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) { return; } - val hsadminCredentialsContext = newCredentialsEntity.getLoginContexts().stream() - .filter(HsCredentialsContext::isHsadminContext) + val hsadminProfileScope = newProfileEntity.getScopes().stream() + .filter(HsProfileScope::isHsadminScope) .toList(); - if (hsadminCredentialsContext.isEmpty()) { + if (hsadminProfileScope.isEmpty()) { throw new ValidationException( messageTranslator.translate( - "credentials.own-hsadmin-credentials-must-not-be-removed" + "profile.own-hsadmin-profile-must-not-be-removed" )); } } - private static String toDisplay(final List contextsWhichRequireNaturalPerson) { - return contextsWhichRequireNaturalPerson.stream() - .map(HsCredentialsContext::toShortString) + private static String toDisplay(final List scopesWhichRequireNaturalPerson) { + return scopesWhichRequireNaturalPerson.stream() + .map(HsProfileScope::toShortString) .sorted() .map(s -> "'" + s + "'") .collect(Collectors.joining(", ")); @@ -301,14 +282,14 @@ public class HsCredentialsController implements CredentialsApi { return newRbacSubject; } - private List findByPersonUuid(final UUID personUuid) { + private List findByPersonUuid(final UUID personUuid) { val person = realPersonRepo.findByUuid(personUuid).orElseThrow( () -> new EntityNotFoundException( messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", personUuid) ) ); - return credentialsRepo.findByPerson(person); + return profileRepo.findByPerson(person); } @@ -323,9 +304,7 @@ public class HsCredentialsController implements CredentialsApi { return result; } - final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { - ofNullable(entity.getLastUsed()).ifPresent( - dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC))); + final BiConsumer ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> { of(entity.getSubject()).ifPresent( subject -> resource.setNickname(subject.getName()) ); @@ -335,25 +314,25 @@ public class HsCredentialsController implements CredentialsApi { ) ); - resource.setContexts(mapToValidContextResources(entity)); + resource.setScopes(mapToValidScopeResources(entity)); }; - private List mapToValidContextResources(final HsCredentialsEntity entity) { - var allContexts = mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class); - return allContexts.stream() - .filter(context -> !context.getOnlyForNaturalPersons() || + private List mapToValidScopeResources(final HsProfileEntity entity) { + var allScopes = mapper.mapList(entity.getScopes().stream().toList(), ScopeResource.class); + return allScopes.stream() + .filter(scope -> !scope.getOnlyForNaturalPersons() || entity.getPerson().getPersonType() == HsOfficePersonType.NATURAL_PERSON) .toList(); } - final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { + final BiConsumer RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> { val person = realPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow( () -> new EntityNotFoundException( messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", resource.getPersonUuid()) ) ); - entity.setLoginContexts(contextMapper.mapCredentialsToContextEntities(resource.getContexts())); + entity.setScopes(scopeMapper.mapProfileToScopeEntities(resource.getScopes())); entity.setPerson(person); }; diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntity.java similarity index 64% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntity.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntity.java index aa7411fb..91849565 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntity.java @@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.repr.Stringify; import net.hostsharing.hsadminng.repr.Stringifyable; // import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship -import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -20,20 +19,20 @@ import static jakarta.persistence.CascadeType.REFRESH; import static net.hostsharing.hsadminng.repr.Stringify.stringify; @Entity -@Table(schema = "hs_accounts", name = "credentials") +@Table(schema = "hs_accounts", name = "profile") @Getter @Setter @Builder @NoArgsConstructor @AllArgsConstructor -public class HsCredentialsEntity implements BaseEntity, Stringifyable { +public class HsProfileEntity implements BaseEntity, Stringifyable { - protected static Stringify stringify = stringify(HsCredentialsEntity.class, "credentials") - .withProp(HsCredentialsEntity::isActive) - .withProp(HsCredentialsEntity::getEmailAddress) - .withProp(HsCredentialsEntity::getTotpSecrets) - .withProp(HsCredentialsEntity::getPhonePassword) - .withProp(HsCredentialsEntity::getSmsNumber) + protected static Stringify stringify = stringify(HsProfileEntity.class, "profile") + .withProp(HsProfileEntity::isActive) + .withProp(HsProfileEntity::getEmailAddress) + .withProp(HsProfileEntity::getTotpSecrets) + .withProp(HsProfileEntity::getPhonePassword) + .withProp(HsProfileEntity::getSmsNumber) .quotedValues(false); @Id @@ -46,14 +45,11 @@ public class HsCredentialsEntity implements BaseEntity, Str @ManyToOne(optional = false, fetch = FetchType.EAGER) @JoinColumn(name = "person_uuid", nullable = false, updatable = false, referencedColumnName = "uuid") - private HsOfficePersonRealEntity person; // TODO.impl: add RBAC-Support to CredentialsEntity, see Story # + private HsOfficePersonRealEntity person; // TODO.impl: add RBAC-Support to ProfileEntity, see Story # @Version private int version; - @Column - private LocalDateTime lastUsed; - @Column private boolean active; @@ -63,9 +59,6 @@ public class HsCredentialsEntity implements BaseEntity, Str @Column private Integer globalGid; - @Column - private String onboardingToken; - @Column private List totpSecrets; @@ -80,17 +73,17 @@ public class HsCredentialsEntity implements BaseEntity, Str @OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH }) @JoinTable( - name = "context_mapping", schema = "hs_accounts", - joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"), - inverseJoinColumns = @JoinColumn(name = "context_uuid", referencedColumnName = "uuid") + name = "scope_mapping", schema = "hs_accounts", + joinColumns = @JoinColumn(name = "profile_uuid", referencedColumnName = "uuid"), + inverseJoinColumns = @JoinColumn(name = "scope_uuid", referencedColumnName = "uuid") ) - private Set loginContexts; + private Set scopes; - public Set getLoginContexts() { - if ( loginContexts == null ) { - loginContexts = new HashSet<>(); + public Set getScopes() { + if ( scopes == null ) { + scopes = new HashSet<>(); } - return loginContexts; + return scopes; } public void setSubject(final RbacSubjectEntity subject) { diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcher.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcher.java similarity index 57% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcher.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcher.java index 3e424e47..c2d05192 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcher.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcher.java @@ -1,23 +1,23 @@ package net.hostsharing.hsadminng.hs.accounts; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource; import net.hostsharing.hsadminng.mapper.EntityPatcher; import net.hostsharing.hsadminng.mapper.OptionalFromJson; import java.util.Optional; -public class HsCredentialsEntityPatcher implements EntityPatcher { +public class HsProfileEntityPatcher implements EntityPatcher { - private CredentialContextResourceToEntityMapper contextMapper; - private final HsCredentialsEntity entity; + private ScopeResourceToEntityMapper scopeMapper; + private final HsProfileEntity entity; - public HsCredentialsEntityPatcher(final CredentialContextResourceToEntityMapper contextMapper, final HsCredentialsEntity entity) { - this.contextMapper = contextMapper; + public HsProfileEntityPatcher(final ScopeResourceToEntityMapper scopeMapper, final HsProfileEntity entity) { + this.scopeMapper = scopeMapper; this.entity = entity; } @Override - public void apply(final CredentialsPatchResource resource) { + public void apply(final ProfilePatchResource resource) { if ( resource.getActive() != null ) { entity.setActive(resource.getActive()); } @@ -29,8 +29,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher { + + @Timed("app.login.profile.repo.findByUuid") + Optional findByUuid(final UUID uuid); + + @Timed("app.login.profile.repo.findByPerson") + List findByPerson(final HsOfficePerson personUuid); + + @Timed("app.login.profile.repo.findByCurrentSubject") + @Query(nativeQuery = true, value = """ + WITH RECURSIVE + same_person AS ( + SELECT own_profile.person_uuid + FROM hs_accounts.profile own_profile + WHERE own_profile.uuid = rbac.currentSubjectUuid() + ), + represented_persons AS ( + SELECT relation.anchorUuid person_uuid + FROM hs_office.relation relation + WHERE relation.type = 'REPRESENTATIVE' + AND relation.holderUuid IN (SELECT person_uuid FROM same_person) + ) + SELECT DISTINCT profile.* + FROM hs_accounts.profile profile + WHERE profile.person_uuid IN (SELECT person_uuid FROM same_person) + OR profile.person_uuid IN (SELECT person_uuid FROM represented_persons) + """) + List findByCurrentSubject(); + + @Timed("app.login.profile.repo.save") + HsProfileEntity save(final HsProfileEntity entity); +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContext.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScope.java similarity index 79% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContext.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScope.java index c1ed0cc0..c39bd783 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContext.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScope.java @@ -27,14 +27,16 @@ import static net.hostsharing.hsadminng.repr.Symbol.symbol; @AllArgsConstructor(access = AccessLevel.PROTECTED) @SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true) @MappedSuperclass -public abstract class HsCredentialsContext implements Stringifyable, BaseEntity { +public abstract class HsProfileScope implements Stringifyable, BaseEntity { - private static Stringify stringify = stringify(HsCredentialsContext.class, "loginContext") - .withProp(HsCredentialsContext::getType) - .withProp(HsCredentialsContext::getQualifier) - .withProp(HsCredentialsContext::isOnlyForNaturalPersons, + private static Stringify stringify = stringify(HsProfileScope.class, "scope") + .withProp(HsProfileScope::getType) + .withProp(HsProfileScope::getQualifier) + .withProp( + HsProfileScope::isOnlyForNaturalPersons, value -> value ? symbol("NP-ONLY") : null) - .withProp(HsCredentialsContext::isPublicAccess, + .withProp( + HsProfileScope::isPublicAccess, value -> value ? symbol("PUBLIC") : symbol("INTERNAL")) .quotedValues(false) .withSeparator(":"); @@ -61,7 +63,7 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity< @Column(name = "public_access") private boolean publicAccess; - public boolean isHsadminContext() { + public boolean isHsadminScope() { return "HSADMIN".equals(type); } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeController.java similarity index 65% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeController.java index fbcb360d..6cabba3b 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsController.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeController.java @@ -6,8 +6,8 @@ import io.micrometer.core.annotation.Timed; import lombok.val; import net.hostsharing.hsadminng.config.NoSecurityRequirement; import net.hostsharing.hsadminng.rbac.context.Context; -import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ContextsApi; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ScopesApi; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource; import net.hostsharing.hsadminng.mapper.StrictMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @NoSecurityRequirement -public class HsCredentialsContextsController implements ContextsApi { +public class HsProfileScopeController implements ScopesApi { @Autowired private Context context; @@ -26,20 +26,20 @@ public class HsCredentialsContextsController implements ContextsApi { private StrictMapper mapper; @Autowired - private HsCredentialsContextRbacRepository contextRepo; + private HsProfileScopeRbacRepository scopeRepo; @Override @Transactional(readOnly = true) - @Timed("app.credentials.contexts.getListOfLoginContexts") - public ResponseEntity> getListOfContexts(final String assumedRoles) { + @Timed("app.accounts.scopes.getListOfScopes") + public ResponseEntity> getListOfScopes(final String assumedRoles) { if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) { context.assumeRoles(assumedRoles); } val isGlobalAdmin = context.isGlobalAdmin(); - final var loginContexts = contextRepo.findAll().stream().filter( - context -> context.isPublicAccess() || isGlobalAdmin + final var scopes = scopeRepo.findAll().stream().filter( + scope -> scope.isPublicAccess() || isGlobalAdmin ).toList(); - final var result = mapper.mapList(loginContexts, ContextResource.class); + final var result = mapper.mapList(scopes, ScopeResource.class); return ResponseEntity.ok(result); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntity.java similarity index 88% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntity.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntity.java index 00fc3d70..c6170b9f 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntity.java @@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.WITHOUT_IMPLICIT import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor; @Entity -@Table(schema = "hs_accounts", name = "context") // TODO_impl: RBAC rules for _rv do not yet work properly +@Table(schema = "hs_accounts", name = "scope") // TODO_impl: RBAC rules for _rv do not yet work properly @SuperBuilder(toBuilder = true) @Getter @Setter @@ -32,11 +32,11 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor; @AttributeOverrides({ @AttributeOverride(name = "uuid", column = @Column(name = "uuid")) }) -public class HsCredentialsContextRbacEntity extends HsCredentialsContext { +public class HsProfileScopeRbacEntity extends HsProfileScope { // TODO_impl: RBAC rules for _rv do not yet work properly (remove the X) public static RbacSpec rbacX() { - return rbacViewFor("credentialsContext", HsCredentialsContextRbacEntity.class) + return rbacViewFor("profileScope", HsProfileScopeRbacEntity.class) .withIdentityView(SQL.projection("type || ':' || qualifier")) .withRestrictedViewOrderBy(SQL.expression("type || ':' || qualifier")) .withoutUpdatableColumns() @@ -50,6 +50,6 @@ public class HsCredentialsContextRbacEntity extends HsCredentialsContext { // TODO_impl: RBAC rules for _rv do not yet work properly (remove the X) public static void mainX(String[] args) throws IOException { - rbacX().generateWithBaseFileName("9-hs-global/950-accounts/9513-hs-credentials-rbac"); + rbacX().generateWithBaseFileName("9-hs-global/950-accounts/9513-hs-profile-rbac"); } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepository.java new file mode 100644 index 00000000..b4b6a204 --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepository.java @@ -0,0 +1,24 @@ +package net.hostsharing.hsadminng.hs.accounts; + +import io.micrometer.core.annotation.Timed; +import org.springframework.data.repository.Repository; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface HsProfileScopeRbacRepository extends Repository { + + @Timed("app.accounts.scope.repo.findAll.rbac") + List findAll(); + + @Timed("app.accounts.scope.repo.findByUuid.rbac") + Optional findByUuid(final UUID id); + + @Timed("app.accounts.scope.repo.findByTypeAndQualifier.rbac") + Optional findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier); + + @Timed("app.accounts.scope.repo.save.rbac") + HsProfileScopeRbacEntity save(final HsProfileScopeRbacEntity entity); +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntity.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntity.java similarity index 81% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntity.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntity.java index 746bc760..ea8a6080 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntity.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntity.java @@ -11,7 +11,7 @@ import lombok.Setter; import lombok.experimental.SuperBuilder; @Entity -@Table(schema = "hs_accounts", name = "context") +@Table(schema = "hs_accounts", name = "scope") @SuperBuilder(toBuilder = true) @Getter @Setter @@ -19,5 +19,5 @@ import lombok.experimental.SuperBuilder; @AttributeOverrides({ @AttributeOverride(name = "uuid", column = @Column(name = "uuid")) }) -public class HsCredentialsContextRealEntity extends HsCredentialsContext { +public class HsProfileScopeRealEntity extends HsProfileScope { } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepository.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepository.java new file mode 100644 index 00000000..4748d94c --- /dev/null +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepository.java @@ -0,0 +1,24 @@ +package net.hostsharing.hsadminng.hs.accounts; + +import io.micrometer.core.annotation.Timed; +import org.springframework.data.repository.Repository; + +import jakarta.validation.constraints.NotNull; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface HsProfileScopeRealRepository extends Repository { + + @Timed("app.account.scope.repo.findAll.real") + List findAll(); + + @Timed("app.account.scope.repo.findByUuid.real") + Optional findByUuid(final UUID id); + + @Timed("app.account.scope.repo.findByTypeAndQualifier.real") + Optional findByTypeAndQualifier(@NotNull String type, @NotNull String qualifier); + + @Timed("app.account.scope.repo.save.real") + HsProfileScopeRealEntity save(final HsProfileScopeRealEntity entity); +} diff --git a/src/main/java/net/hostsharing/hsadminng/hs/accounts/CredentialContextResourceToEntityMapper.java b/src/main/java/net/hostsharing/hsadminng/hs/accounts/ScopeResourceToEntityMapper.java similarity index 52% rename from src/main/java/net/hostsharing/hsadminng/hs/accounts/CredentialContextResourceToEntityMapper.java rename to src/main/java/net/hostsharing/hsadminng/hs/accounts/ScopeResourceToEntityMapper.java index 3807ed6e..6d8fdd87 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/accounts/CredentialContextResourceToEntityMapper.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/accounts/ScopeResourceToEntityMapper.java @@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.accounts; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityNotFoundException; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource; import net.hostsharing.hsadminng.config.MessageTranslator; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -14,56 +14,56 @@ import java.util.Set; import java.util.stream.Collectors; @Service -public class CredentialContextResourceToEntityMapper { +public class ScopeResourceToEntityMapper { private final EntityManager em; private final MessageTranslator messageTranslator; @Autowired - public CredentialContextResourceToEntityMapper(EntityManager em, MessageTranslator messageTranslator) { + public ScopeResourceToEntityMapper(final EntityManager em, final MessageTranslator messageTranslator) { this.em = em; this.messageTranslator = messageTranslator; } - public Set mapCredentialsToContextEntities( - List resources + public Set mapProfileToScopeEntities( + final List resources ) { - final var entities = new HashSet(); - syncCredentialsContextEntities(resources, entities); + final var entities = new HashSet(); + syncProfileScopeEntities(resources, entities); return entities; } - public void syncCredentialsContextEntities( - List resources, - Set entities + public void syncProfileScopeEntities( + final List resources, + final Set entities ) { final var resourceUuids = resources.stream() - .map(ContextResource::getUuid) + .map(ScopeResource::getUuid) .collect(Collectors.toSet()); final var entityUuids = entities.stream() - .map(HsCredentialsContextRealEntity::getUuid) + .map(HsProfileScopeRealEntity::getUuid) .collect(Collectors.toSet()); entities.removeIf(e -> !resourceUuids.contains(e.getUuid())); for (final var resource : resources) { if (!entityUuids.contains(resource.getUuid())) { - final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid()); - if (existingContextEntity == null) { + final var existingScopeEntity = em.find(HsProfileScopeRealEntity.class, resource.getUuid()); + if (existingScopeEntity == null) { throw new EntityNotFoundException( messageTranslator.translate( "general.{0}-{1}-not-found-or-not-accessible", - "credentials uuid", resource.getUuid())); + "profile uuid", resource.getUuid())); } - if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) || - (resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) { + if ((resource.getType() != null && !existingScopeEntity.getType().equals(resource.getType())) || + (resource.getQualifier() != null && !existingScopeEntity.getQualifier().equals(resource.getQualifier()))) { throw new EntityNotFoundException( messageTranslator.translate( - "credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}", - existingContextEntity, resource)); + "profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}", + existingScopeEntity, resource)); } - entities.add(existingContextEntity); + entities.add(existingScopeEntity); } } } diff --git a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java index ef2783bc..486fe404 100644 --- a/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java +++ b/src/main/java/net/hostsharing/hsadminng/hs/validation/ValidatableProperty.java @@ -58,7 +58,7 @@ public abstract class ValidatableProperty

, T private BiFunction computedBy; @Accessors(makeFinal = true, chain = true, fluent = false) - private ComputeMode computed; // name 'computed' instead 'computeMode' for better readability in property description + private ComputeMode computed; // name 'computed' instead of 'computeMode' for better readability in property description @Accessors(makeFinal = true, chain = true, fluent = false) private boolean readOnly; diff --git a/src/main/resources/api-definition/accounts/api-mappings.yaml b/src/main/resources/api-definition/accounts/api-mappings.yaml index 4a0eb2ed..fe611dff 100644 --- a/src/main/resources/api-definition/accounts/api-mappings.yaml +++ b/src/main/resources/api-definition/accounts/api-mappings.yaml @@ -13,5 +13,5 @@ map: - type: string:uuid => java.util.UUID paths: - /api/hs/accounts/credentials/{credentialsUuid}: + /api/hs/accounts/profiles/{profileUuid}: null: org.openapitools.jackson.nullable.JsonNullable diff --git a/src/main/resources/api-definition/accounts/api-paths.yaml b/src/main/resources/api-definition/accounts/api-paths.yaml index b33c4512..9cecc5c8 100644 --- a/src/main/resources/api-definition/accounts/api-paths.yaml +++ b/src/main/resources/api-definition/accounts/api-paths.yaml @@ -13,20 +13,17 @@ paths: /api/hs/accounts/current: $ref: "current.yaml" - # Contexts + # Scopes - /api/hs/accounts/contexts: - $ref: "contexts.yaml" + /api/hs/accounts/scopes: + $ref: "scopes.yaml" - # Credentials + # Profile - /api/hs/accounts/credentials/{credentialsUuid}/used: - $ref: "credentials-with-uuid-used.yaml" + /api/hs/accounts/profiles/{profileUuid}: + $ref: "profile-with-uuid.yaml" - /api/hs/accounts/credentials/{credentialsUuid}: - $ref: "credentials-with-uuid.yaml" - - /api/hs/accounts/credentials: - $ref: "credentials.yaml" + /api/hs/accounts/profiles: + $ref: "profiles.yaml" diff --git a/src/main/resources/api-definition/accounts/credentials-with-uuid-used.yaml b/src/main/resources/api-definition/accounts/credentials-with-uuid-used.yaml deleted file mode 100644 index 64c3fd46..00000000 --- a/src/main/resources/api-definition/accounts/credentials-with-uuid-used.yaml +++ /dev/null @@ -1,23 +0,0 @@ -post: - tags: - - credentials - description: 'Is called when credentials got used for a login.' - operationId: credentialsUsed - parameters: - - name: credentialsUuid - in: path - required: true - schema: - type: string - format: uuid - responses: - "200": - description: OK - content: - 'application/json': - schema: - $ref: 'credentials-schemas.yaml#/components/schemas/Credentials' - "401": - $ref: 'error-responses.yaml#/components/responses/Unauthorized' - "403": - $ref: 'error-responses.yaml#/components/responses/Forbidden' diff --git a/src/main/resources/api-definition/accounts/current.yaml b/src/main/resources/api-definition/accounts/current.yaml index df63ec28..c73ed64f 100644 --- a/src/main/resources/api-definition/accounts/current.yaml +++ b/src/main/resources/api-definition/accounts/current.yaml @@ -2,7 +2,7 @@ get: summary: Currently logged in user data. description: Returns information about the currently logged in user. tags: - - credentials + - profile operationId: getCurrentLoginUser responses: "200": @@ -10,7 +10,7 @@ get: content: 'application/json': schema: - $ref: 'credentials-schemas.yaml#/components/schemas/CurrentLoginUser' + $ref: 'profile-schemas.yaml#/components/schemas/CurrentLoginUser' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": diff --git a/src/main/resources/api-definition/accounts/credentials-schemas.yaml b/src/main/resources/api-definition/accounts/profile-schemas.yaml similarity index 82% rename from src/main/resources/api-definition/accounts/credentials-schemas.yaml rename to src/main/resources/api-definition/accounts/profile-schemas.yaml index 95b8e018..c263cc1d 100644 --- a/src/main/resources/api-definition/accounts/credentials-schemas.yaml +++ b/src/main/resources/api-definition/accounts/profile-schemas.yaml @@ -13,7 +13,7 @@ components: globalAdmin: type: boolean - Credentials: + Profile: type: object properties: uuid: @@ -40,22 +40,17 @@ components: type: number globalGid: type: number - onboardingToken: - type: string - contexts: + scopes: type: array items: - $ref: 'context-schemas.yaml#/components/schemas/Context' - lastUsed: - type: string - format: date-time + $ref: 'scope-schemas.yaml#/components/schemas/Scope' required: - uuid - active - - contexts + - scopes additionalProperties: false - CredentialsPatch: + ProfilePatch: type: object properties: totpSecrets: @@ -73,13 +68,13 @@ components: nullable: true active: type: boolean - contexts: + scopes: type: array items: - $ref: 'context-schemas.yaml#/components/schemas/Context' + $ref: 'scope-schemas.yaml#/components/schemas/Scope' additionalProperties: false - CredentialsInsert: + ProfileInsert: type: object properties: person.uuid: @@ -88,12 +83,6 @@ components: nickname: type: string pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname - totpSecrets: - type: array - items: - type: string - phonePassword: - type: string emailAddress: type: string smsNumber: @@ -104,12 +93,16 @@ components: type: number globalGid: type: number - onboardingToken: + phonePassword: type: string - contexts: + totpSecrets: type: array items: - $ref: 'context-schemas.yaml#/components/schemas/Context' + type: string + scopes: + type: array + items: + $ref: 'scope-schemas.yaml#/components/schemas/Scope' required: - person.uuid - nickname diff --git a/src/main/resources/api-definition/accounts/credentials-with-uuid.yaml b/src/main/resources/api-definition/accounts/profile-with-uuid.yaml similarity index 60% rename from src/main/resources/api-definition/accounts/credentials-with-uuid.yaml rename to src/main/resources/api-definition/accounts/profile-with-uuid.yaml index 07590f0b..ddb08d93 100644 --- a/src/main/resources/api-definition/accounts/credentials-with-uuid.yaml +++ b/src/main/resources/api-definition/accounts/profile-with-uuid.yaml @@ -1,23 +1,23 @@ get: tags: - - credentials - description: 'Fetch a single credentials its uuid, if visible for the current subject.' - operationId: getSingleCredentialsByUuid + - profile + description: 'Fetch a single profile its uuid, if visible for the current subject.' + operationId: getSingleProfileByUuid parameters: - - name: credentialsUuid + - name: profileUuid in: path required: true schema: type: string format: uuid - description: UUID of the credentials to fetch. + description: UUID of the profile to fetch. responses: "200": description: OK content: 'application/json': schema: - $ref: 'credentials-schemas.yaml#/components/schemas/Credentials' + $ref: 'profile-schemas.yaml#/components/schemas/Profile' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' @@ -26,11 +26,11 @@ get: patch: tags: - - credentials - description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.' - operationId: patchCredentials + - profile + description: 'Updates a single profile identified by its uuid, if permitted for the current subject.' + operationId: patchProfile parameters: - - name: credentialsUuid + - name: profileUuid in: path required: true schema: @@ -40,14 +40,14 @@ patch: content: 'application/json': schema: - $ref: 'credentials-schemas.yaml#/components/schemas/CredentialsPatch' + $ref: 'profile-schemas.yaml#/components/schemas/ProfilePatch' responses: "200": description: OK content: 'application/json': schema: - $ref: 'credentials-schemas.yaml#/components/schemas/Credentials' + $ref: 'profile-schemas.yaml#/components/schemas/Profile' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": @@ -55,17 +55,17 @@ patch: delete: tags: - - credentials - description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.' - operationId: deleteCredentialsByUuid + - profile + description: 'Delete a single profile identified by its uuid, if permitted for the current subject.' + operationId: deleteProfileByUuid parameters: - - name: credentialsUuid + - name: profileUuid in: path required: true schema: type: string format: uuid - description: UUID of the credentials to delete. + description: UUID of the profile to delete. responses: "204": description: No Content diff --git a/src/main/resources/api-definition/accounts/credentials.yaml b/src/main/resources/api-definition/accounts/profiles.yaml similarity index 62% rename from src/main/resources/api-definition/accounts/credentials.yaml rename to src/main/resources/api-definition/accounts/profiles.yaml index b7ad86a0..5e4c3dec 100644 --- a/src/main/resources/api-definition/accounts/credentials.yaml +++ b/src/main/resources/api-definition/accounts/profiles.yaml @@ -1,9 +1,9 @@ get: - summary: Returns a list of all credentials. - description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles. + summary: Returns a list of all profile. + description: Returns the list of all profile which are visible to the current subject or any of it's assumed roles. tags: - - credentials - operationId: getListOfCredentials + - profile + operationId: getListOfProfile parameters: - $ref: 'auth.yaml#/components/parameters/assumedRoles' - name: personUuid @@ -12,7 +12,7 @@ get: schema: type: string format: uuid - description: The UUID of the person, whose credentials are to be fetched. Or null, if all credentials of the login-use should be fetched. + description: The UUID of the person, whose profile are to be fetched. Or null, if all profile of the login-use should be fetched. responses: "200": description: OK @@ -21,31 +21,31 @@ get: schema: type: array items: - $ref: 'credentials-schemas.yaml#/components/schemas/Credentials' + $ref: 'profile-schemas.yaml#/components/schemas/Profile' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": $ref: 'error-responses.yaml#/components/responses/Forbidden' post: - summary: Adds a new credentials. + summary: Adds a new profile. tags: - - credentials - operationId: postNewCredentials + - profile + operationId: postNewProfile requestBody: description: A JSON object describing the new credential. required: true content: application/json: schema: - $ref: 'credentials-schemas.yaml#/components/schemas/CredentialsInsert' + $ref: 'profile-schemas.yaml#/components/schemas/ProfileInsert' responses: "201": description: Created content: 'application/json': schema: - $ref: 'credentials-schemas.yaml#/components/schemas/Credentials' + $ref: 'profile-schemas.yaml#/components/schemas/Profile' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": diff --git a/src/main/resources/api-definition/accounts/context-schemas.yaml b/src/main/resources/api-definition/accounts/scope-schemas.yaml similarity index 96% rename from src/main/resources/api-definition/accounts/context-schemas.yaml rename to src/main/resources/api-definition/accounts/scope-schemas.yaml index 3dc85d60..08aabb14 100644 --- a/src/main/resources/api-definition/accounts/context-schemas.yaml +++ b/src/main/resources/api-definition/accounts/scope-schemas.yaml @@ -3,7 +3,7 @@ components: schemas: - Context: + Scope: type: object properties: uuid: diff --git a/src/main/resources/api-definition/accounts/contexts.yaml b/src/main/resources/api-definition/accounts/scopes.yaml similarity index 60% rename from src/main/resources/api-definition/accounts/contexts.yaml rename to src/main/resources/api-definition/accounts/scopes.yaml index f5ce1b33..878ba007 100644 --- a/src/main/resources/api-definition/accounts/contexts.yaml +++ b/src/main/resources/api-definition/accounts/scopes.yaml @@ -1,9 +1,9 @@ get: - summary: Returns a list of all accessible contexts. - description: Returns the list of all contexts which are visible to the current subject or any of it's assumed roles. + summary: Returns a list of all accessible scopes. + description: Returns the list of all scopes which are visible to the current subject or any of it's assumed roles. tags: - - contexts - operationId: getListOfContexts + - scopes + operationId: getListOfScopes parameters: - $ref: 'auth.yaml#/components/parameters/assumedRoles' responses: @@ -14,7 +14,7 @@ get: schema: type: array items: - $ref: 'context-schemas.yaml#/components/schemas/Context' + $ref: 'scope-schemas.yaml#/components/schemas/Scope' "401": $ref: 'error-responses.yaml#/components/responses/Unauthorized' "403": diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e2ab281f..d73adc4a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -75,7 +75,7 @@ metrics: # HOWTO set logging-levels for certain Java packages (trace, debug, info, warn, error) logging: level: -# org.springframework.security: info + org.springframework.security: info # org.hibernate.SQL: DEBUG # Enable SQL query logging # org.hibernate.orm.jdbc.bind: TRACE # Enable SQL parameter binding logging # org.springframework.web: DEBUG diff --git a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql index 17de7c11..84095a87 100644 --- a/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql +++ b/src/main/resources/db/changelog/1-rbac/1050-rbac-base.sql @@ -31,34 +31,41 @@ end; $$; --// -- ============================================================================ ---changeset michael.hoennig:rbac-base-SUBJECT endDelimiter:--// +--changeset michael.hoennig:rbac-base-SUBJECT runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- -/* +DO $$ + BEGIN + IF NOT EXISTS (SELECT FROM information_schema.tables + WHERE table_schema = 'rbac' AND table_name = 'subject') THEN - */ -create table rbac.subject -( - uuid uuid primary key references rbac.reference (uuid) on delete cascade, - name varchar(63) not null unique -); + CREATE TABLE rbac.subject + ( + uuid uuid primary key references rbac.reference (uuid) on delete cascade, + name varchar(63) not null unique + ); -call base.create_journal('rbac.subject'); + CALL base.create_journal('rbac.subject'); + END IF; + END +$$; create or replace function rbac.create_subject(subjectName varchar) returns uuid returns null on null input language plpgsql as $$ declare - objectId uuid; + stableUuidNamespace uuid; + subjectUuid uuid; begin + stableUuidNamespace := '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid; + subjectUuid := uuid_generate_v5(stableUuidNamespace, subjectName); insert - into rbac.reference (type) - values ('rbac.subject') - returning uuid into objectId; + into rbac.reference (uuid, type) + values (subjectUuid, 'rbac.subject'); insert into rbac.subject (uuid, name) - values (objectid, subjectName); - return objectId; + values (subjectUuid, subjectName); + return subjectUuid; end; $$; diff --git a/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data-for-accounts.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data-for-accounts.sql index 5e67c268..6e15665e 100644 --- a/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data-for-accounts.sql +++ b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data-for-accounts.sql @@ -4,15 +4,15 @@ -- I presume it's a bug in Liquibase that other changeset checksums are changed by new changesets in the same file -- ============================================================================ ---changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION-FOR-CREDENTIALS context:!without-test-data endDelimiter:--// +--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATION-FOR-PROFILES context:!without-test-data runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- do language plpgsql $$ begin - call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex'); - call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran'); - call hs_office.person_create_test_data('NP', null, 'User', 'Drew'); - call hs_office.person_create_test_data('NP', null, 'User', 'Test'); + call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex', true); + call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran', true); + call hs_office.person_create_test_data('NP', null, 'User', 'Drew', true); + call hs_office.person_create_test_data('NP', null, 'User', 'Test', true); end; $$; --// diff --git a/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql index 1be4c25d..4c53aad5 100644 --- a/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql +++ b/src/main/resources/db/changelog/5-hs-office/502-person/5028-hs-office-person-test-data.sql @@ -2,7 +2,7 @@ -- ============================================================================ ---changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATOR endDelimiter:--// +--changeset michael.hoennig:hs-office-person-TEST-DATA-GENERATOR runOnChange:true validCheckSum:ANY endDelimiter:--// -- ---------------------------------------------------------------------------- /* @@ -12,7 +12,8 @@ create or replace procedure hs_office.person_create_test_data( newPersonType hs_office.PersonType, newTradeName varchar, newFamilyName varchar = null, - newGivenName varchar = null + newGivenName varchar = null, + ignoreIfExists boolean = false ) language plpgsql as $$ declare @@ -22,7 +23,10 @@ begin fullName := concat_ws(', ', newTradeName, newFamilyName, newGivenName); emailAddr = 'person-' || left(base.cleanIdentifier(fullName), 32) || '@example.com'; call base.defineContext('creating person test-data'); - perform rbac.create_subject(emailAddr); + if ignoreIfExists and exists (select 1 from rbac.subject where name = emailAddr) then + return; + end if; + perform rbac.create_subject(emailAddr);call base.defineContext('creating person test-data', null, emailAddr); call base.defineContext('creating person test-data', null, emailAddr); raise notice 'creating test person: % by %', fullName, emailAddr; diff --git a/src/main/resources/db/changelog/9-hs-global/950-accounts/9500-hs-accounts-schema.sql b/src/main/resources/db/changelog/9-hs-global/950-accounts/9500-hs-accounts-schema.sql index ae415fdf..cac77a3f 100644 --- a/src/main/resources/db/changelog/9-hs-global/950-accounts/9500-hs-accounts-schema.sql +++ b/src/main/resources/db/changelog/9-hs-global/950-accounts/9500-hs-accounts-schema.sql @@ -2,7 +2,7 @@ -- ============================================================================ ---changeset michael.hoennig:hs-credentials-SCHEMA endDelimiter:--// +--changeset michael.hoennig:hs-profile-SCHEMA endDelimiter:--// -- ---------------------------------------------------------------------------- CREATE SCHEMA hs_accounts; --// diff --git a/src/main/resources/db/changelog/9-hs-global/950-accounts/9510-hs-accounts.sql b/src/main/resources/db/changelog/9-hs-global/950-accounts/9510-hs-accounts.sql index 1ffcee9a..122b4105 100644 --- a/src/main/resources/db/changelog/9-hs-global/950-accounts/9510-hs-accounts.sql +++ b/src/main/resources/db/changelog/9-hs-global/950-accounts/9510-hs-accounts.sql @@ -2,10 +2,10 @@ -- ============================================================================ ---changeset michael.hoennig:hs-credentials-CREDENTIALS-TABLE endDelimiter:--// +--changeset michael.hoennig:hs-profile-PROFILE-TABLE endDelimiter:--// -- ---------------------------------------------------------------------------- -create table hs_accounts.credentials +create table hs_accounts.profile ( uuid uuid PRIMARY KEY references rbac.subject (uuid) initially deferred, version int not null default 0, @@ -13,10 +13,8 @@ create table hs_accounts.credentials person_uuid uuid not null references hs_office.person(uuid), active bool, - last_used timestamp, global_uid int unique, -- w/o global_gid int unique, -- w/o - onboarding_token text, -- w/o, but can be set to null to invalidate totp_secrets text[], phone_password text, @@ -27,10 +25,10 @@ create table hs_accounts.credentials -- ============================================================================ ---changeset michael.hoennig:hs-credentials-context-CONTEXT-TABLE endDelimiter:--// +--changeset michael.hoennig:hs-profile-scope-SCOPE-TABLE endDelimiter:--// -- ---------------------------------------------------------------------------- -create table hs_accounts.context +create table hs_accounts.scope ( uuid uuid PRIMARY KEY, version int not null default 0, @@ -48,31 +46,31 @@ create table hs_accounts.context -- ============================================================================ ---changeset michael.hoennig:hs-credentials-CONTEXT-IMMUTABLE-TRIGGER endDelimiter:--// +--changeset michael.hoennig:hs-profile-SCOPE-IMMUTABLE-TRIGGER endDelimiter:--// -- ---------------------------------------------------------------------------- -CREATE OR REPLACE FUNCTION hs_accounts.prevent_context_update() +CREATE OR REPLACE FUNCTION hs_accounts.prevent_scope_update() RETURNS TRIGGER AS $$ BEGIN - RAISE EXCEPTION 'Updates to hs_accounts.context are not allowed.'; + RAISE EXCEPTION 'Updates to hs_accounts.scope are not allowed.'; END; $$ LANGUAGE plpgsql; -- Trigger to enforce immutability -CREATE TRIGGER context_immutable_trigger -BEFORE UPDATE ON hs_accounts.context -FOR EACH ROW EXECUTE FUNCTION hs_accounts.prevent_context_update(); +CREATE TRIGGER scope_immutable_trigger +BEFORE UPDATE ON hs_accounts.scope +FOR EACH ROW EXECUTE FUNCTION hs_accounts.prevent_scope_update(); --// -- ============================================================================ ---changeset michael.hoennig:hs_accounts-CONTEXT-MAPPING endDelimiter:--// +--changeset michael.hoennig:hs_accounts-SCOPE-MAPPING endDelimiter:--// -- ---------------------------------------------------------------------------- -create table hs_accounts.context_mapping +create table hs_accounts.scope_mapping ( uuid uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - credentials_uuid uuid references hs_accounts.credentials(uuid) ON DELETE CASCADE, - context_uuid uuid references hs_accounts.context(uuid) ON DELETE RESTRICT + profile_uuid uuid references hs_accounts.profile(uuid) ON DELETE CASCADE, + scope_uuid uuid references hs_accounts.scope(uuid) ON DELETE RESTRICT ); --// @@ -81,16 +79,16 @@ create table hs_accounts.context_mapping --changeset michael.hoennig:hs-hs_accounts-JOURNALS endDelimiter:--// -- ---------------------------------------------------------------------------- -call base.create_journal('hs_accounts.context_mapping'); -call base.create_journal('hs_accounts.context'); -call base.create_journal('hs_accounts.credentials'); +call base.create_journal('hs_accounts.scope_mapping'); +call base.create_journal('hs_accounts.scope'); +call base.create_journal('hs_accounts.profile'); --// -- ============================================================================ --changeset michael.hoennig:hs_accounts-HISTORICIZATION endDelimiter:--// -- ---------------------------------------------------------------------------- -call base.tx_create_historicization('hs_accounts.context_mapping'); -call base.tx_create_historicization('hs_accounts.context'); -call base.tx_create_historicization('hs_accounts.credentials'); +call base.tx_create_historicization('hs_accounts.scope_mapping'); +call base.tx_create_historicization('hs_accounts.scope'); +call base.tx_create_historicization('hs_accounts.profile'); --// diff --git a/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-context-rbac.md b/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-context-rbac.md deleted file mode 100644 index f3959949..00000000 --- a/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-context-rbac.md +++ /dev/null @@ -1,41 +0,0 @@ -### rbac credentialsContext - -This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. - -```mermaid -%%{init:{'flowchart':{'htmlLabels':false}}}%% -flowchart TB - -subgraph credentialsContext["`**credentialsContext**`"] - direction TB - style credentialsContext fill:#dd4901,stroke:#274d6e,stroke-width:8px - - subgraph credentialsContext:roles[ ] - style credentialsContext:roles fill:#dd4901,stroke:white - - role:credentialsContext:OWNER[[credentialsContext:OWNER]] - role:credentialsContext:ADMIN[[credentialsContext:ADMIN]] - role:credentialsContext:REFERRER[[credentialsContext:REFERRER]] - end - - subgraph credentialsContext:permissions[ ] - style credentialsContext:permissions fill:#dd4901,stroke:white - - perm:credentialsContext:INSERT{{credentialsContext:INSERT}} - perm:credentialsContext:UPDATE{{credentialsContext:UPDATE}} - perm:credentialsContext:DELETE{{credentialsContext:DELETE}} - perm:credentialsContext:SELECT{{credentialsContext:SELECT}} - end -end - -%% granting roles to roles -role:credentialsContext:OWNER ==> role:credentialsContext:ADMIN -role:credentialsContext:ADMIN ==> role:credentialsContext:REFERRER - -%% granting permissions to roles -role:rbac.global:ADMIN ==> perm:credentialsContext:INSERT -role:rbac.global:ADMIN ==> perm:credentialsContext:UPDATE -role:rbac.global:ADMIN ==> perm:credentialsContext:DELETE -role:rbac.global:REFERRER ==> perm:credentialsContext:SELECT - -``` diff --git a/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-scope-rbac.md b/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-scope-rbac.md new file mode 100644 index 00000000..443c9f55 --- /dev/null +++ b/src/main/resources/db/changelog/9-hs-global/950-accounts/9513-hs-accounts-scope-rbac.md @@ -0,0 +1,41 @@ +### rbac profileContext + +This code generated was by RbacViewMermaidFlowchartGenerator, do not amend manually. + +```mermaid +%%{init:{'flowchart':{'htmlLabels':false}}}%% +flowchart TB + +subgraph profileContext["`**profileContext**`"] + direction TB + style profileContext fill:#dd4901,stroke:#274d6e,stroke-width:8px + + subgraph profileContext:roles[ ] + style profileContext:roles fill:#dd4901,stroke:white + + role:profileContext:OWNER[[profileContext:OWNER]] + role:profileContext:ADMIN[[profileContext:ADMIN]] + role:profileContext:REFERRER[[profileContext:REFERRER]] + end + + subgraph profileContext:permissions[ ] + style profileContext:permissions fill:#dd4901,stroke:white + + perm:profileContext:INSERT{{profileContext:INSERT}} + perm:profileContext:UPDATE{{profileContext:UPDATE}} + perm:profileContext:DELETE{{profileContext:DELETE}} + perm:profileContext:SELECT{{profileContext:SELECT}} + end +end + +%% granting roles to roles +role:profileContext:OWNER ==> role:profileContext:ADMIN +role:profileContext:ADMIN ==> role:profileContext:REFERRER + +%% granting permissions to roles +role:rbac.global:ADMIN ==> perm:profileContext:INSERT +role:rbac.global:ADMIN ==> perm:profileContext:UPDATE +role:rbac.global:ADMIN ==> perm:profileContext:DELETE +role:rbac.global:REFERRER ==> perm:profileContext:SELECT + +``` diff --git a/src/main/resources/db/changelog/9-hs-global/950-accounts/9519-hs-accounts-test-data.sql b/src/main/resources/db/changelog/9-hs-global/950-accounts/9519-hs-accounts-test-data.sql index 857a3f0b..a6133c8c 100644 --- a/src/main/resources/db/changelog/9-hs-global/950-accounts/9519-hs-accounts-test-data.sql +++ b/src/main/resources/db/changelog/9-hs-global/950-accounts/9519-hs-accounts-test-data.sql @@ -2,7 +2,7 @@ -- ============================================================================ ---changeset michael.hoennig:hs_accounts-credentials-TEST-DATA context:!without-test-data endDelimiter:--// +--changeset michael.hoennig:hs_accounts-profile-TEST-DATA context:!without-test-data endDelimiter:--// -- ---------------------------------------------------------------------------- do language plpgsql $$ @@ -16,11 +16,11 @@ declare personDrewUuid uuid; - context_HSADMIN_prod hs_accounts.context; - context_SSH_internal hs_accounts.context; - context_SSH_external hs_accounts.context; - context_MATRIX_internal hs_accounts.context; - context_MATRIX_external hs_accounts.context; + scope_HSADMIN_prod hs_accounts.scope; + scope_SSH_internal hs_accounts.scope; + scope_SSH_external hs_accounts.scope; + scope_MATRIX_internal hs_accounts.scope; + scope_MATRIX_external hs_accounts.scope; begin call base.defineContext('creating booking-project test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN'); @@ -32,28 +32,28 @@ begin userDrewSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='selfregistered-user-drew@hostsharing.org'); personDrewUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Drew'); - -- Add test contexts - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + -- Add test scopes + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true) - RETURNING * INTO context_HSADMIN_prod; - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + RETURNING * INTO scope_HSADMIN_prod; + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false) - RETURNING * INTO context_SSH_internal; - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + RETURNING * INTO scope_SSH_internal; + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('33333333-3333-3333-3333-333333333333', 'SSH', 'external', false, true) - RETURNING * INTO context_SSH_external; - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + RETURNING * INTO scope_SSH_external; + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('44444444-4444-4444-4444-444444444444', 'MATRIX', 'internal', true, false) - RETURNING * INTO context_MATRIX_internal; - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + RETURNING * INTO scope_MATRIX_internal; + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('55555555-5555-5555-5555-555555555555', 'MATRIX', 'external', true, true) - RETURNING * INTO context_MATRIX_external; - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + RETURNING * INTO scope_MATRIX_external; + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('66666666-6666-6666-6666-666666666666', 'MASTODON', 'external', false, true); - INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES + INSERT INTO hs_accounts.scope (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES ('77777777-7777-7777-7777-777777777777', 'BBB', 'external', false, true); --- grant general access to public credential contexts +-- grant general access to public credential scopes -- TODO_impl: RBAC rules for _rv do not yet work properly -- call rbac.grantPermissiontoRole( -- rbac.createPermission(context_HSADMIN_prod.uuid, 'SELECT'), @@ -64,25 +64,25 @@ begin -- call rbac.grantPermissionToRole( -- rbac.createPermission(context_MATRIX_internal.uuid, 'SELECT'), -- rbac.global_ADMIN()); --- call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_SSH_internal), rbac.global_ADMIN()); --- call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); +-- call rbac.grantRoleToRole(hs_accounts.scope_REFERRER(context_SSH_internal), rbac.global_ADMIN()); +-- call rbac.grantRoleToRole(hs_accounts.scope_REFERRER(context_MATRIX_internal), rbac.global_ADMIN()); - -- Add test credentials (linking to assumed rbac.subject UUIDs) - INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secrets, phone_password, email_address, sms_number) VALUES - ( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'), - ( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'), - ( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, 'token-def', ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777'); + -- Add test profile (linking to assumed rbac.subject UUIDs) + INSERT INTO hs_accounts.profile (uuid, version, person_uuid, active, global_uid, global_gid, totp_secrets, phone_password, email_address, sms_number) VALUES + ( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'), + ( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'), + ( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777'); - -- Map credentials to contexts - INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES - (superuserAlexSubjectUuid, context_HSADMIN_prod.uuid), - (superuserFranSubjectUuid, context_HSADMIN_prod.uuid), - (userDrewSubjectUuid, context_HSADMIN_prod.uuid), - (superuserAlexSubjectUuid, context_SSH_internal.uuid), - (superuserFranSubjectUuid, context_SSH_internal.uuid), - (userDrewSubjectUuid, context_SSH_external.uuid), - (superuserAlexSubjectUuid, context_MATRIX_internal.uuid), - (superuserFranSubjectUuid, context_MATRIX_internal.uuid); + -- Map profile to contexts + INSERT INTO hs_accounts.scope_mapping (profile_uuid, scope_uuid) VALUES + (superuserAlexSubjectUuid, scope_HSADMIN_prod.uuid), + (superuserFranSubjectUuid, scope_HSADMIN_prod.uuid), + (userDrewSubjectUuid, scope_HSADMIN_prod.uuid), + (superuserAlexSubjectUuid, scope_SSH_internal.uuid), + (superuserFranSubjectUuid, scope_SSH_internal.uuid), + (userDrewSubjectUuid, scope_SSH_external.uuid), + (superuserAlexSubjectUuid, scope_MATRIX_internal.uuid), + (superuserFranSubjectUuid, scope_MATRIX_internal.uuid); end; $$; --// diff --git a/src/main/resources/i18n/messages_de.properties b/src/main/resources/i18n/messages_de.properties index 1c87e510..a7e97fa6 100644 --- a/src/main/resources/i18n/messages_de.properties +++ b/src/main/resources/i18n/messages_de.properties @@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - auf Deutsch test.ponged-{0}--in-your-language=ponged {0} - auf Deutsch test.available-in-all-properties-files=Hallo {0} - DE! -# authorization -auth.cas-service-ticket-could-not-be-verified=CAS Service-Ticket konnte nicht verifiziert werden -auth.unknown-authorization-ticket=unbekanntes Autorisierungs-Ticket - # general validations general.{0}-{1}-not-found={0} "{1}" nicht gefunden general.{0}-{1}-not-found-or-not-accessible={0} "{1}" nicht gefunden oder nicht zugänglich general.but-is=ist aber -# credentials validations -credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1} -credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert -credentials.access-denied-for-contexts-{0}=Kontext-Zugriff verweigert: {0} -credentials.context-requires-natural-person-{0}=Kontext verlangt eine natürliche Person: {0} -credentials.own-hsadmin-credentials-must-not-be-removed=die eigenen hsadmin-Credentials dürfen nicht entfernt werden +# profile validations +profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}=existierender Gültigkeitsbereich {0} passt nicht zum angegebenen {1} +profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert +profile.access-denied-for-scopes-{0}=Zugriff auf Geltungsbereich verweigert: {0} +profile.scope-requires-natural-person-{0}=Geltungsbereich verlangt eine natürliche Person: {0} +profile.own-hsadmin-profile-must-not-be-removed=die eigenen hsadmin-Profile dürfen nicht entfernt werden # office.coop-shares office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1} diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties index 2cd1db1b..76aea01d 100644 --- a/src/main/resources/i18n/messages_en.properties +++ b/src/main/resources/i18n/messages_en.properties @@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - in English test.ponged-{0}--in-your-language=ponged {0} - in English test.available-in-all-properties-files=Hello {0} - EN! -# authorization -auth.cas-service-ticket-could-not-be-verified=CAS service-ticket could not be verified -auth.unknown-authorization-ticket=unknown authorization ticket - # general validations general.{0}-{1}-not-found={0} "{1}" not found general.{0}-{1}-not-found-or-not-accessible={0} "{1}" not found or not accessible general.but-is=but is -# credentials validations -credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1} -credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person -credentials.access-denied-for-contexts-{0}=context-access denied: {0} -credentials.context-requires-natural-person-{0}=context requires natural person: {0} -credentials.own-hsadmin-credentials-must-not-be-removed=own hsadmin-credentials must not be removed +# profile validations +profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1} +profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person +profile.access-denied-for-scopes-{0}=scope-access denied: {0} +profile.scope-requires-natural-person-{0}=scope requires natural person: {0} +profile.own-hsadmin-profile-must-not-be-removed=own hsadmin-profile must not be removed # office.coop-shares office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=for transactiontType {0} shareCount must be positive but is {1} diff --git a/src/main/resources/i18n/messages_fr.properties b/src/main/resources/i18n/messages_fr.properties index 649600c6..94f7b30e 100644 --- a/src/main/resources/i18n/messages_fr.properties +++ b/src/main/resources/i18n/messages_fr.properties @@ -5,21 +5,17 @@ test.pinged--in-your-language=ponged {0} - en Francais test.available-in-all-properties-files=Salut {0} - FR! -# authorization -auth.cas-service-ticket-could-not-be-verified=Le ticket de service CAS n'a pas pu être vérifié -auth.unknown-authorization-ticket=ticket d'autorisation inconnu - # general validations general.{0}-{1}-not-found={0} "{1}" non trouvé general.{0}-{1}-not-found-or-not-accessible={0} "{1}" non trouvé ou non accessible general.but-is=mais c'est -# credentials validations -credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1} -credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée -credentials.access-denied-for-contexts-{0}=accès au contexte refusé : {0} -credentials.context-requires-natural-person-{0}=le contexte requiert une personne physique : {0} -credentials.own-hsadmin-credentials-must-not-be-removed=suppression des identifiants hsadmin propres interdite +# profile validations +profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1} +profile.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée +profile.access-denied-for-scopes-{0}=accès au domaine d'application refusé : {0} +profile.scope-requires-natural-person-{0}=le domaine d'application requiert une personne physique : {0} +profile.own-hsadmin-profile-must-not-be-removed=suppression des identifiants hsadmin propres interdite # office.coop-shares office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=pour le type de transaction {0}, shareCount doit être positif mais est {1} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcherUnitTest.java deleted file mode 100644 index 2f1f69fd..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsEntityPatcherUnitTest.java +++ /dev/null @@ -1,168 +0,0 @@ -package net.hostsharing.hsadminng.hs.accounts; - -import net.hostsharing.hsadminng.config.MessageTranslator; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource; -import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import jakarta.persistence.EntityManager; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; -import java.util.stream.Stream; - -import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; - -@TestInstance(PER_CLASS) -@ExtendWith(MockitoExtension.class) -class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase< - CredentialsPatchResource, - HsCredentialsEntity - > { - - private static final UUID INITIAL_CREDENTIALS_UUID = UUID.randomUUID(); - - private static final Boolean INITIAL_ACTIVE = true; - private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com"; - private static final List INITIAL_TOTP_SECRETS = List.of("initial_2fa"); - private static final String INITIAL_SMS_NUMBER = "initial_sms"; - private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw"; - - private static final Boolean PATCHED_ACTIVE = false; - private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com"; - private static final List PATCHED_TOTP_SECRETS = List.of("patched_2fa"); - private static final String PATCHED_SMS_NUMBER = "patched_sms"; - private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw"; - - // Contexts - private static final UUID CONTEXT_UUID_1 = UUID.randomUUID(); - private static final UUID CONTEXT_UUID_2 = UUID.randomUUID(); - private static final UUID CONTEXT_UUID_3 = UUID.randomUUID(); - - private final HsCredentialsContextRealEntity initialContextEntity1 = HsCredentialsContextRealEntity.builder() - .uuid(CONTEXT_UUID_1) - .type("HSADMIN") - .qualifier("prod") - .build(); - private final HsCredentialsContextRealEntity initialContextEntity2 = HsCredentialsContextRealEntity.builder() - .uuid(CONTEXT_UUID_2) - .type("SSH") - .qualifier("dev") - .build(); - - private ContextResource patchContextResource2; - private ContextResource patchContextResource3; - - // This is what em.find should return for CONTEXT_UUID_3 - private final HsCredentialsContextRealEntity newContextEntity3 = HsCredentialsContextRealEntity.builder() - .uuid(CONTEXT_UUID_3) - .type("HSADMIN") - .qualifier("test") - .build(); - - private final Set initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2); - private List patchedContextResources; - private final Set expectedPatchedContextEntities = Set.of(initialContextEntity2, newContextEntity3); - - @Mock - private EntityManager em; - - @BeforeEach - void initMocks() { - // Mock em.find for contexts that are part of the patch and need to be fetched - lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_1))).thenReturn(initialContextEntity1); - lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_2))).thenReturn(initialContextEntity2); - lenient().when(em.find(eq(HsCredentialsContextRealEntity.class), eq(CONTEXT_UUID_3))).thenReturn(newContextEntity3); - - patchContextResource2 = new ContextResource(); - patchContextResource2.setUuid(CONTEXT_UUID_2); - patchContextResource2.setType("SSH"); - patchContextResource2.setQualifier("dev"); - - patchContextResource3 = new ContextResource(); - patchContextResource3.setUuid(CONTEXT_UUID_3); - patchContextResource3.setType("HSADMIN"); - patchContextResource3.setQualifier("test"); - - patchedContextResources = List.of(patchContextResource2, patchContextResource3); - } - - @Override - protected HsCredentialsEntity newInitialEntity() { - final var entity = new HsCredentialsEntity(); - entity.setUuid(INITIAL_CREDENTIALS_UUID); - entity.setActive(INITIAL_ACTIVE); - entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); - entity.setTotpSecrets(INITIAL_TOTP_SECRETS); - entity.setSmsNumber(INITIAL_SMS_NUMBER); - entity.setPhonePassword(INITIAL_PHONE_PASSWORD); - // Ensure loginContexts is a mutable set for the patcher - entity.setLoginContexts(new HashSet<>(initialContextEntities)); - return entity; - } - - @Override - protected CredentialsPatchResource newPatchResource() { - return new CredentialsPatchResource(); - } - - @Override - protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) { - final var contextMapper = new CredentialContextResourceToEntityMapper(em, mock(MessageTranslator.class)); - return new HsCredentialsEntityPatcher(contextMapper, entity); - } - - @Override - protected Stream propertyTestDescriptors() { - return Stream.of( - new SimpleProperty<>( - "active", - CredentialsPatchResource::setActive, - PATCHED_ACTIVE, - HsCredentialsEntity::setActive, - PATCHED_ACTIVE) - .notNullable(), - new JsonNullableProperty<>( - "emailAddress", - CredentialsPatchResource::setEmailAddress, - PATCHED_EMAIL_ADDRESS, - HsCredentialsEntity::setEmailAddress, - PATCHED_EMAIL_ADDRESS), - new SimpleProperty<>( - "totpSecret", - CredentialsPatchResource::setTotpSecrets, - PATCHED_TOTP_SECRETS, - HsCredentialsEntity::setTotpSecrets, - PATCHED_TOTP_SECRETS) - .notNullable(), - new JsonNullableProperty<>( - "smsNumber", - CredentialsPatchResource::setSmsNumber, - PATCHED_SMS_NUMBER, - HsCredentialsEntity::setSmsNumber, - PATCHED_SMS_NUMBER), - new JsonNullableProperty<>( - "phonePassword", - CredentialsPatchResource::setPhonePassword, - PATCHED_PHONE_PASSWORD, - HsCredentialsEntity::setPhonePassword, - PATCHED_PHONE_PASSWORD), - new SimpleProperty<>( - "contexts", - CredentialsPatchResource::setContexts, - patchedContextResources, - HsCredentialsEntity::setLoginContexts, - expectedPatchedContextEntities) - .notNullable() - ); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileControllerAcceptanceTest.java similarity index 65% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsControllerAcceptanceTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileControllerAcceptanceTest.java index 95e64e48..2195161e 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileControllerAcceptanceTest.java @@ -4,7 +4,7 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; import lombok.val; import net.hostsharing.hsadminng.rbac.context.Context; -import net.hostsharing.hsadminng.hs.accounts.HsCredentialsEntity.HsCredentialsEntityBuilder; +import net.hostsharing.hsadminng.hs.accounts.HsProfileEntity.HsProfileEntityBuilder; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity; import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository; import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity; @@ -27,7 +27,6 @@ import jakarta.persistence.PersistenceContext; import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer; import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON; @@ -36,9 +35,6 @@ import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; @Tag("generalIntegrationTest") @Transactional @@ -47,7 +43,7 @@ import static org.hamcrest.Matchers.nullValue; ) @ActiveProfiles("fake-jwt") // too complex database interaction for just a RestTest, thus a fully integrated test -class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup { +class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort Integer port; @@ -62,13 +58,13 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup HsOfficePersonRealRepository realPersonRepo; @Autowired - HsCredentialsContextRealRepository contextRepo; + HsProfileScopeRealRepository scopeRepo; @Autowired - HsCredentialsRepository credentialsRepo; + HsProfileRepository profileRepo; @Autowired - HsCredentialsContextRbacRepository loginContextRbacRepo; + HsProfileScopeRbacRepository scopeRbacRepo; @Autowired JpaAttempt jpaAttempt; @@ -105,23 +101,23 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested - class GetCredentialsByUuid { + class GetProfileByUuid { @Test - void shouldFilterInvalidContextsRegardingNonNaturalPerson() { + void shouldFilterInvalidScopesRegardingNonNaturalPerson() { // given val legalPerson = givenLegalPerson("selfregistered-user-drew@hostsharing.org"); - val credentialsEntity = givenNewCredentials("selfregistered-user-drew@hostsharing.org", + val profileEntity = givenNewProfile("selfregistered-user-drew@hostsharing.org", "test-subject1", legalPerson, builder -> { - builder.loginContexts(new HashSet<>(contextRepo.findAll())); + builder.scopes(new HashSet<>(scopeRepo.findAll())); }); RestAssured // @formatter:off .given() - .header("Authorization", bearer(credentialsEntity.getSubject().getName())) + .header("Authorization", bearer(profileEntity.getSubject().getName())) .port(port) .when() - .get("http://localhost/api/hs/accounts/credentials/" + credentialsEntity.getUuid()) + .get("http://localhost/api/hs/accounts/profiles/" + profileEntity.getUuid()) .then().log().all().assertThat() .statusCode(200) .contentType("application/json") @@ -143,8 +139,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup "active": false, "globalUid": null, "globalGid": null, - "onboardingToken": null, - "contexts": [ + "scopes": [ { "uuid": "33333333-3333-3333-3333-333333333333", "type": "SSH", @@ -166,8 +161,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup "onlyForNaturalPersons": false, "publicAccess": true } - ], - "lastUsed": null + ] } """)); // @formatter:on @@ -175,14 +169,14 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Nested - class PostNewCredentials { + class PostNewProfile { @Test - void shouldRejectCreatingCredentialsForUnrepresentedPerson() { + void shouldRejectCreatingProfileForUnrepresentedPerson() { // given val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org"); - val publicContext = contextRepo.findByTypeAndQualifier("SSH", "external").orElseThrow(); - assertThat(publicContext.isPublicAccess()).as("precondition failed").isTrue(); + val publicScope = scopeRepo.findByTypeAndQualifier("SSH", "external").orElseThrow(); + assertThat(publicScope.isPublicAccess()).as("precondition failed").isTrue(); RestAssured // @formatter:off .given() @@ -196,16 +190,16 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup "active": true, "globalUid": 30001, "globalGid": 40001, - "contexts": [ + "scopes": [ { "uuid" : "%s" } ] } - """.formatted(testPerson.getUuid(), publicContext.getUuid())) + """.formatted(testPerson.getUuid(), publicScope.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/accounts/credentials") + .post("http://localhost/api/hs/accounts/profiles") .then().log().all().assertThat() .statusCode(400) .contentType("application/json") @@ -214,15 +208,15 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup } @Test - void shouldRejectCreatingCredentialsWithPrivateContextForNormalUser() { + void shouldRejectCreatingProfileWithPrivateScopeForNormalUser() { // given val drewPerson = realPersonRepo.findPersonByOptionalNameLike("Drew").getFirst(); - val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal") - .map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow(); - val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal") - .map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow(); - val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external") - .map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow(); + val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal") + .map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow(); + val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal") + .map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow(); + val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external") + .map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow(); RestAssured // @formatter:off .given() @@ -236,7 +230,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup "active": true, "globalUid": 30001, "globalGid": 40001, - "contexts": [ + "scopes": [ { "uuid" : "%s" }, { "uuid" : "%s" }, { "uuid" : "%s" } @@ -244,25 +238,25 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup } """.formatted( drewPerson.getUuid(), - publicExternalMatrixContext.getUuid(), - privateInternalSshContext.getUuid(), - privateInternalMatrixContext.getUuid())) + publicExternalMatrixScope.getUuid(), + privateInternalSshScope.getUuid(), + privateInternalMatrixScope.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/accounts/credentials") + .post("http://localhost/api/hs/accounts/profiles") .then().log().all().assertThat() .statusCode(400) .contentType("application/json") - .body("message", containsString("Kontext-Zugriff verweigert: 'MATRIX:internal', 'SSH:internal'")); + .body("message", containsString("Zugriff auf Geltungsbereich verweigert: 'MATRIX:internal', 'SSH:internal'")); // @formatter:on } @Test - void shouldRejectCreatingCredentialsWithNaturalPersonRequirementForNonNaturalPerson() { + void shouldRejectCreatingProfileWithNaturalPersonRequirementForNonNaturalPerson() { // given val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst(); - val hsadminProdContextOnlyForNaturalPersons = contextRepo.findByTypeAndQualifier("HSADMIN", "prod") - .map(HsCredentialsControllerAcceptanceTest::asNaturalPersonContext).orElseThrow(); + val hsadminProdScopeOnlyForNaturalPersons = scopeRepo.findByTypeAndQualifier("HSADMIN", "prod") + .map(HsProfileControllerAcceptanceTest::asNaturalPersonScope).orElseThrow(); RestAssured // @formatter:off .given() @@ -276,39 +270,39 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup "active": true, "globalUid": 30001, "globalGid": 40001, - "contexts": [ + "scopes": [ { "uuid" : "%s" } ] } """.formatted( firstGmbHPerson.getUuid(), - hsadminProdContextOnlyForNaturalPersons.getUuid())) + hsadminProdScopeOnlyForNaturalPersons.getUuid())) .port(port) .when() - .post("http://localhost/api/hs/accounts/credentials") + .post("http://localhost/api/hs/accounts/profiles") .then().log().all().assertThat() .statusCode(400) .contentType("application/json") - .body("message", containsString("Kontext verlangt eine natürliche Person: 'HSADMIN:prod'")); + .body("message", containsString("Geltungsbereich verlangt eine natürliche Person: 'HSADMIN:prod'")); // @formatter:on } } @Nested - class PatchCredentials { + class PatchProfile { @Test - void shouldRejectPatchingCredentialsWithPrivateContextForNormalUser() { + void shouldRejectPatchingProfileWithPrivateScopeForNormalUser() { // given context.define("selfregistered-user-drew@hostsharing.org"); - val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow() + val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow() .getSubject().getUuid(); - val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal") - .map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow(); - val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal") - .map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow(); - val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external") - .map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow(); + val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal") + .map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow(); + val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal") + .map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow(); + val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external") + .map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow(); RestAssured // @formatter:off .given() @@ -317,34 +311,34 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { - "contexts": [ + "scopes": [ { "uuid" : "%s" }, { "uuid" : "%s" }, { "uuid" : "%s" } ] } """.formatted( - privateInternalSshContext.getUuid(), - publicExternalMatrixContext.getUuid(), - privateInternalMatrixContext.getUuid())) + privateInternalSshScope.getUuid(), + publicExternalMatrixScope.getUuid(), + privateInternalMatrixScope.getUuid())) .port(port) .when() - .patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid) + .patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid) .then().log().all().assertThat() .statusCode(400) .contentType("application/json") - .body("message", containsString("Kontext-Zugriff verweigert: 'MATRIX:internal', 'SSH:internal'")); + .body("message", containsString("Zugriff auf Geltungsbereich verweigert: 'MATRIX:internal', 'SSH:internal'")); // @formatter:on } @Test - void shouldRejectPatchingCredentialsAndRemovingTheOwnHsadminCredentials() { + void shouldRejectPatchingProfileAndRemovingTheOwnHsadminProfile() { // given context.define("selfregistered-user-drew@hostsharing.org"); - val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow() + val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow() .getSubject().getUuid(); - val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external") - .map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow(); + val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external") + .map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow(); RestAssured // @formatter:off .given() @@ -353,49 +347,18 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup .contentType(ContentType.JSON) .body(""" { - "contexts": [ + "scopes": [ { "uuid" : "%s" } ] } - """.formatted(publicExternalMatrixContext.getUuid())) + """.formatted(publicExternalMatrixScope.getUuid())) .port(port) .when() - .patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid) + .patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid) .then().log().all().assertThat() .statusCode(400) .contentType("application/json") - .body("message", containsString("die eigenen hsadmin-Credentials dürfen nicht entfernt werden")); - // @formatter:on - } - } - - @Nested - class MarkCredentialsAsUsed { - - @Test - void markCredentialsAsUsed() { - // given - val testPerson = givenNaturalPerson("selfregistered-user-drew@hostsharing.org"); - val credentialsEntity = givenNewCredentials("selfregistered-user-drew@hostsharing.org", - "test-subject2", - testPerson, builder -> { - builder.onboardingToken("some-onboarding-token"); - builder.loginContexts(contextRepo.findAll().stream() - .filter(HsCredentialsContext::isPublicAccess).collect(Collectors.toSet())); - }); - - RestAssured // @formatter:off - .given() - .header("Authorization", bearer("superuser-alex@hostsharing.net")) - .port(port) - .when() - .post("http://localhost/api/hs/accounts/credentials/" + credentialsEntity.getUuid() + "/used") - .then().log().all().assertThat() - .statusCode(200) - .contentType("application/json") - .body("uuid", is(credentialsEntity.getUuid().toString())) - .body("onboardingToken", is(nullValue())) - .body("lastUsed", is(not(nullValue()))); + .body("message", containsString("die eigenen hsadmin-Profile dürfen nicht entfernt werden")); // @formatter:on } } @@ -433,25 +396,25 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup }).returnedValue(); } - private static HsCredentialsContextRealEntity asNaturalPersonContext(@NotNull HsCredentialsContextRealEntity context) { - assertThat(context.isOnlyForNaturalPersons()).as("precondition failed").isTrue(); - return context; + private static HsProfileScopeRealEntity asNaturalPersonScope(@NotNull HsProfileScopeRealEntity scope) { + assertThat(scope.isOnlyForNaturalPersons()).as("precondition failed").isTrue(); + return scope; } - private static HsCredentialsContextRealEntity asPrivateContext(@NotNull HsCredentialsContextRealEntity context) { - assertThat(context.isPublicAccess()).as("precondition failed").isFalse(); - return context; + private static HsProfileScopeRealEntity asPrivateScope(@NotNull HsProfileScopeRealEntity scope) { + assertThat(scope.isPublicAccess()).as("precondition failed").isFalse(); + return scope; } - private static HsCredentialsContextRealEntity asPublicContext(@NotNull HsCredentialsContextRealEntity context) { - assertThat(context.isPublicAccess()).as("precondition failed").isTrue(); - return context; + private static HsProfileScopeRealEntity asPublicScope(@NotNull HsProfileScopeRealEntity scope) { + assertThat(scope.isPublicAccess()).as("precondition failed").isTrue(); + return scope; } - private HsCredentialsEntity givenNewCredentials( + private HsProfileEntity givenNewProfile( final String executingSubjectName, final String newSubjectName, final HsOfficePersonRealEntity person, - final Consumer modifier + final Consumer modifier ) { return jpaAttempt.transacted(() -> { context.define(executingSubjectName); @@ -462,12 +425,12 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup context.define(subject.getName()); val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid()); - val credentialsBuilder = HsCredentialsEntity.builder() + val profileBuilder = HsProfileEntity.builder() .person(attachedPerson) .subject(subjectRepo.findByUuid(subject.getUuid())) - .loginContexts(Set.of()); - modifier.accept(credentialsBuilder); - return toCleanup(credentialsRepo.save(credentialsBuilder.build())); + .scopes(Set.of()); + modifier.accept(profileBuilder); + return toCleanup(profileRepo.save(profileBuilder.build())); }).assertSuccessful().returnedValue(); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcherUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcherUnitTest.java new file mode 100644 index 00000000..b3e2331b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileEntityPatcherUnitTest.java @@ -0,0 +1,167 @@ +package net.hostsharing.hsadminng.hs.accounts; + +import lombok.val; +import net.hostsharing.hsadminng.config.MessageTranslator; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource; +import net.hostsharing.hsadminng.rbac.test.PatchUnitTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import jakarta.persistence.EntityManager; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; + +@TestInstance(PER_CLASS) +@ExtendWith(MockitoExtension.class) +class HsProfileEntityPatcherUnitTest extends PatchUnitTestBase< + ProfilePatchResource, + HsProfileEntity + > { + + private static final UUID INITIAL_PROFILE_UUID = UUID.randomUUID(); + + private static final Boolean INITIAL_ACTIVE = true; + private static final String INITIAL_EMAIL_ADDRESS = "initial@example.com"; + private static final List INITIAL_TOTP_SECRETS = List.of("initial_2fa"); + private static final String INITIAL_SMS_NUMBER = "initial_sms"; + private static final String INITIAL_PHONE_PASSWORD = "initial_phone_pw"; + + private static final Boolean PATCHED_ACTIVE = false; + private static final String PATCHED_EMAIL_ADDRESS = "patched@example.com"; + private static final List PATCHED_TOTP_SECRETS = List.of("patched_2fa"); + private static final String PATCHED_SMS_NUMBER = "patched_sms"; + private static final String PATCHED_PHONE_PASSWORD = "patched_phone_pw"; + + // Scopes + private static final UUID SCOPE_UUID_1 = UUID.randomUUID(); + private static final UUID SCOPE_UUID_2 = UUID.randomUUID(); + private static final UUID SCOPE_UUID_3 = UUID.randomUUID(); + + private final HsProfileScopeRealEntity initialScopeEntity1 = HsProfileScopeRealEntity.builder() + .uuid(SCOPE_UUID_1) + .type("HSADMIN") + .qualifier("prod") + .build(); + private final HsProfileScopeRealEntity initialScopeEntity2 = HsProfileScopeRealEntity.builder() + .uuid(SCOPE_UUID_2) + .type("SSH") + .qualifier("dev") + .build(); + + // This is what em.find should return for SCOPE_UUID_3 + private final HsProfileScopeRealEntity newScopeEntity3 = HsProfileScopeRealEntity.builder() + .uuid(SCOPE_UUID_3) + .type("HSADMIN") + .qualifier("test") + .build(); + + private final Set initialScopeEntities = Set.of(initialScopeEntity1, initialScopeEntity2); + private List patchedScopeResources; + private final Set expectedPatchedScopeEntities = Set.of(initialScopeEntity2, + newScopeEntity3); + + @Mock + private EntityManager em; + + @BeforeEach + void initMocks() { + // Mock em.find for scopes that are part of the patch and need to be fetched + lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_1))).thenReturn(initialScopeEntity1); + lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_2))).thenReturn(initialScopeEntity2); + lenient().when(em.find(eq(HsProfileScopeRealEntity.class), eq(SCOPE_UUID_3))).thenReturn(newScopeEntity3); + + val patchScopeResource2 = new ScopeResource(); + patchScopeResource2.setUuid(SCOPE_UUID_2); + patchScopeResource2.setType("SSH"); + patchScopeResource2.setQualifier("dev"); + + val patchScopeResource3 = new ScopeResource(); + patchScopeResource3.setUuid(SCOPE_UUID_3); + patchScopeResource3.setType("HSADMIN"); + patchScopeResource3.setQualifier("test"); + + patchedScopeResources = List.of(patchScopeResource2, patchScopeResource3); + } + + @Override + protected HsProfileEntity newInitialEntity() { + final var entity = new HsProfileEntity(); + entity.setUuid(INITIAL_PROFILE_UUID); + entity.setActive(INITIAL_ACTIVE); + entity.setEmailAddress(INITIAL_EMAIL_ADDRESS); + entity.setTotpSecrets(INITIAL_TOTP_SECRETS); + entity.setSmsNumber(INITIAL_SMS_NUMBER); + entity.setPhonePassword(INITIAL_PHONE_PASSWORD); + // Ensure scopes is a mutable set for the patcher + entity.setScopes(new HashSet<>(initialScopeEntities)); + return entity; + } + + @Override + protected ProfilePatchResource newPatchResource() { + return new ProfilePatchResource(); + } + + @Override + protected HsProfileEntityPatcher createPatcher(final HsProfileEntity entity) { + final var scopeMapper = new ScopeResourceToEntityMapper(em, mock(MessageTranslator.class)); + return new HsProfileEntityPatcher(scopeMapper, entity); + } + + @Override + protected Stream propertyTestDescriptors() { + return Stream.of( + new SimpleProperty<>( + "active", + ProfilePatchResource::setActive, + PATCHED_ACTIVE, + HsProfileEntity::setActive, + PATCHED_ACTIVE) + .notNullable(), + new JsonNullableProperty<>( + "emailAddress", + ProfilePatchResource::setEmailAddress, + PATCHED_EMAIL_ADDRESS, + HsProfileEntity::setEmailAddress, + PATCHED_EMAIL_ADDRESS), + new SimpleProperty<>( + "totpSecret", + ProfilePatchResource::setTotpSecrets, + PATCHED_TOTP_SECRETS, + HsProfileEntity::setTotpSecrets, + PATCHED_TOTP_SECRETS) + .notNullable(), + new JsonNullableProperty<>( + "smsNumber", + ProfilePatchResource::setSmsNumber, + PATCHED_SMS_NUMBER, + HsProfileEntity::setSmsNumber, + PATCHED_SMS_NUMBER), + new JsonNullableProperty<>( + "phonePassword", + ProfilePatchResource::setPhonePassword, + PATCHED_PHONE_PASSWORD, + HsProfileEntity::setPhonePassword, + PATCHED_PHONE_PASSWORD), + new SimpleProperty<>( + "scopes", + ProfilePatchResource::setScopes, + patchedScopeResources, + HsProfileEntity::setScopes, + expectedPatchedScopeEntities) + .notNullable() + ); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileRepositoryIntegrationTest.java similarity index 77% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepositoryIntegrationTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileRepositoryIntegrationTest.java index 94c1e281..c9bac4c0 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileRepositoryIntegrationTest.java @@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.catchThrowable; @DataJpaTest @Tag("generalIntegrationTest") @Import({ Context.class, JpaAttempt.class }) -class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup { +class HsProfileRepositoryIntegrationTest extends ContextBasedTestWithCleanup { private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net"; private static final String SUPERUSER_FRAN_SUBJECT_NAME = "superuser-fran@hostsharing.net"; @@ -52,10 +52,10 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup private HsOfficePersonRealRepository personRepo; @Autowired - private HsCredentialsRepository credentialsRepository; + private HsProfileRepository profileRepository; @Autowired - private HsCredentialsContextRealRepository loginContextRealRepo; + private HsProfileScopeRealRepository scopeRealRepo; // fetched UUIDs from test-data private RbacSubjectEntity alexSubject; @@ -76,7 +76,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup @Test public void historizationIsAvailable() { // given - final String nativeQuerySql = "select * from hs_accounts.credentials_hv"; + final String nativeQuerySql = "select * from hs_accounts.profile_hv"; // when historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant())); @@ -85,7 +85,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then assertThat(rowsBefore) - .as("hs_accounts.credentials_hv only contain no rows for a timestamp before test data creation") + .as("hs_accounts.profile_hv only contain no rows for a timestamp before test data creation") .hasSize(0); // and when @@ -95,86 +95,86 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup // then assertThat(rowsAfter) - .as("hs_accounts.credentials_hv should now contain the test-data rows for the current timestamp") + .as("hs_accounts.profile_hv should now contain the test-data rows for the current timestamp") .hasSize(3); } @Test - void representativeShouldFindOwnAndRepresentedCredentialsByCurrentSubject() { + void representativeShouldFindOwnAndRepresentedProfileByCurrentSubject() { // given final var firstGmbHPerson = givenPerson("First GmbH"); givenRelation(REPRESENTATIVE) .withAnchorPersonLike(firstGmbHPerson) .withHolder(drewPerson) .withContact("some test contact"); - givenCredentials() + givenProfile() .forSubject("first-gmbh") .forPerson(firstGmbHPerson) .withEMailAddress("first-gmbh@example.com"); // when - final var foundCredentials = attempt( + final var foundProfile = attempt( em, () -> { context(USER_DREW_SUBJECT_NAME); - return credentialsRepository.findByCurrentSubject(); + return profileRepository.findByCurrentSubject(); }) .assertNotNull().returnedValue(); // then - assertThat(foundCredentials).hasSize(2) - .map(HsCredentialsEntity::getEmailAddress) + assertThat(foundProfile).hasSize(2) + .map(HsProfileEntity::getEmailAddress) .containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com"); } @Test - void globalAdminShouldFindOnlyOwnCredentialsByCurrentSubject() { + void globalAdminShouldFindOnlyOwnProfileByCurrentSubject() { // when - final var foundCredentials = attempt( + final var foundProfile = attempt( em, () -> { context(SUPERUSER_FRAN_SUBJECT_NAME); - return credentialsRepository.findByCurrentSubject(); + return profileRepository.findByCurrentSubject(); }) .assertNotNull().returnedValue(); // then - assertThat(foundCredentials).hasSize(1) - .map(HsCredentialsEntity::getEmailAddress) + assertThat(foundProfile).hasSize(1) + .map(HsProfileEntity::getEmailAddress) .containsExactlyInAnyOrder("fran@example.com"); } @Test void shouldFindByUuidUsingTestData() { // when - final var foundEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid()); + final var foundEntityOptional = profileRepository.findByUuid(alexSubject.getUuid()); // then assertThat(foundEntityOptional).isPresent() - .map(HsCredentialsEntity::getEmailAddress).contains("alex@example.com"); + .map(HsProfileEntity::getEmailAddress).contains("alex@example.com"); } @Test - void shouldSaveCredentialsWithExistingContext() { + void shouldSaveProfileWithExistingScope() { // given - final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod") + final var existingScope = scopeRealRepo.findByTypeAndQualifier("HSADMIN", "prod") .orElseThrow(); - final var newCredentials = HsCredentialsEntity.builder() + final var newProfile = HsProfileEntity.builder() .subject(testUserSubject) .person(testUserPerson) .active(true) .emailAddress("test-user@example.com") .globalUid(2011) .globalGid(2011) - .loginContexts(mutableSetOf(existingContext)) + .scopes(mutableSetOf(existingScope)) .build(); // when - toCleanup(credentialsRepository.save(newCredentials)); + toCleanup(profileRepository.save(newProfile)); em.flush(); em.clear(); // then - final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid()); + final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid()); assertThat(foundEntityOptional).isPresent(); final var foundEntity = foundEntityOptional.get(); assertThat(foundEntity.getEmailAddress()).isEqualTo("test-user@example.com"); @@ -182,29 +182,29 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup assertThat(foundEntity.getVersion()).isEqualTo(0); // Initial version assertThat(foundEntity.getGlobalUid()).isEqualTo(2011); - assertThat(foundEntity.getLoginContexts()).hasSize(1) - .map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)"); + assertThat(foundEntity.getScopes()).hasSize(1) + .map(HsProfileScopeRealEntity::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)"); } @Test - void shouldNotSaveCredentialsWithNewContext() { + void shouldNotSaveProfileWithNewScope() { // given - final var newContext = HsCredentialsContextRealEntity.builder() + final var newScope = HsProfileScopeRealEntity.builder() .type("MATRIX") .qualifier("forbidden") .build(); - final var newCredentials = HsCredentialsEntity.builder() + final var newProfile = HsProfileEntity.builder() .subject(drewSubject) .active(true) .emailAddress("drew.new@example.com") .globalUid(2001) .globalGid(2001) - .loginContexts(mutableSetOf(newContext)) + .scopes(mutableSetOf(newScope)) .build(); // when final var exception = catchThrowable(() -> { - credentialsRepository.save(newCredentials); + profileRepository.save(newProfile); em.flush(); }); @@ -213,9 +213,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup } @Test - void shouldSaveNewCredentialsWithoutContext() { + void shouldSaveNewProfileWithoutScope() { // given - final var newCredentials = HsCredentialsEntity.builder() + final var newProfile = HsProfileEntity.builder() .subject(testUserSubject) .person(testUserPerson) .active(true) @@ -225,37 +225,37 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup .build(); // when - credentialsRepository.save(newCredentials); + profileRepository.save(newProfile); em.flush(); em.clear(); // then - final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid()); + final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid()); assertThat(foundEntityOptional).isPresent(); final var foundEntity = foundEntityOptional.get(); assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com"); assertThat(foundEntity.isActive()).isTrue(); assertThat(foundEntity.getGlobalUid()).isEqualTo(20002); assertThat(foundEntity.getGlobalGid()).isEqualTo(2002); - assertThat(foundEntity.getLoginContexts()).isEmpty(); + assertThat(foundEntity.getScopes()).isEmpty(); } @Test - void shouldUpdateExistingCredentials() { + void shouldUpdateExistingProfile() { // given - final var entityToUpdate = credentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow(); + final var entityToUpdate = profileRepository.findByUuid(alexSubject.getUuid()).orElseThrow(); final var initialVersion = entityToUpdate.getVersion(); // when entityToUpdate.setActive(false); entityToUpdate.setEmailAddress("updated.user1@example.com"); - final var savedEntity = credentialsRepository.save(entityToUpdate); + final var savedEntity = profileRepository.save(entityToUpdate); em.flush(); em.clear(); // then assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion); - final var updatedEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid()); + final var updatedEntityOptional = profileRepository.findByUuid(alexSubject.getUuid()); assertThat(updatedEntityOptional).isPresent(); final var updatedEntity = updatedEntityOptional.get(); assertThat(updatedEntity.isActive()).isFalse(); @@ -307,8 +307,8 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup return new RelationBuilder(relationType); } - private CredentialsBuilder givenCredentials() { - return new CredentialsBuilder(); + private ProfileBuilder givenProfile() { + return new ProfileBuilder(); } private class RelationBuilder { @@ -349,11 +349,11 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup } } - private class CredentialsBuilder { + private class ProfileBuilder { private RbacSubjectEntity subject; private HsOfficePersonRealEntity person; - public CredentialsBuilder forSubject(String subjectName) { + public ProfileBuilder forSubject(String subjectName) { this.subject = RbacSubjectEntity.builder() .name(subjectName) .build(); @@ -362,24 +362,24 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup return this; } - public CredentialsBuilder forPerson(HsOfficePersonRealEntity person) { + public ProfileBuilder forPerson(HsOfficePersonRealEntity person) { this.person = person; return this; } - public HsCredentialsEntity withEMailAddress(String emailAddress) { + public HsProfileEntity withEMailAddress(String emailAddress) { - final var credentials = HsCredentialsEntity.builder() + final var profile = HsProfileEntity.builder() .uuid(subject.getUuid()) .subject(subject) .person(em.find(HsOfficePersonRealEntity.class, person.getUuid())) .emailAddress(emailAddress) .active(true) .build(); - em.persist(credentials); - toCleanup(credentials); + em.persist(profile); + toCleanup(profile); em.flush(); - return credentials; + return profile; } } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsControllerRestTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeControllerRestTest.java similarity index 86% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsControllerRestTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeControllerRestTest.java index d3bc3dc2..42446788 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextsControllerRestTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeControllerRestTest.java @@ -38,13 +38,13 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.SynchronizationType; -@WebMvcTest(HsCredentialsContextsController.class) +@WebMvcTest(HsProfileScopeController.class) @Import({ StrictMapper.class, MessageTranslator.class, JsonObjectMapperConfiguration.class, WebSecurityConfigForWebMvcTests.class }) @ActiveProfiles({"fake-jwt", "test"}) -class HsCredentialsContextsControllerRestTest { +class HsProfileScopeControllerRestTest { @Autowired MockMvc mockMvc; @@ -63,7 +63,7 @@ class HsCredentialsContextsControllerRestTest { EntityManagerFactory emf; @MockitoBean - HsCredentialsContextRbacRepository loginContextRbacRepo; + HsProfileScopeRbacRepository scopeRbacRepo; @TestConfiguration public static class TestConfig { @@ -82,14 +82,14 @@ class HsCredentialsContextsControllerRestTest { } @Test - void getListOfLoginContextsReturnsOkWithEmptyList() throws Exception { + void getListOfScopesReturnsOkWithEmptyList() throws Exception { // given - givenNoContextsInTheRepository(); + givenNoScopesInTheRepository(); // when mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/accounts/contexts") + .get("/api/hs/accounts/scopes") .header("Authorization", bearer("superuser-alex@hostsharing.net")) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) @@ -101,15 +101,15 @@ class HsCredentialsContextsControllerRestTest { } @Test - void getListOfLoginContextsReturnsAllContextsForGlobalAdmin() throws Exception { + void getListOfScopesReturnsAllScopesForGlobalAdmin() throws Exception { // given - givenSomeContextsInTheRepository(); + givenSomeScopesInTheRepository(); when(contextMock.isGlobalAdmin()).thenReturn(true); // when mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/accounts/contexts") + .get("/api/hs/accounts/scopes") .header("Authorization", bearer("Bearer superuser-alex@hostsharing.net")) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) @@ -143,15 +143,15 @@ class HsCredentialsContextsControllerRestTest { } @Test - void getListOfLoginContextsReturnsOnlyPublicContextsForNormalUser() throws Exception { + void getListOfScopesReturnsOnlyPublicScopesForNormalUser() throws Exception { // given - givenSomeContextsInTheRepository(); + givenSomeScopesInTheRepository(); when(contextMock.isGlobalAdmin()).thenReturn(false); // when mockMvc.perform(MockMvcRequestBuilders - .get("/api/hs/accounts/contexts") + .get("/api/hs/accounts/scopes") .header("Authorization", bearer("drew@hostsharing.org")) .accept(MediaType.APPLICATION_JSON)) .andDo(print()) @@ -178,27 +178,27 @@ class HsCredentialsContextsControllerRestTest { ))); } - private void givenNoContextsInTheRepository() { - when(loginContextRbacRepo.findAll()).thenReturn(emptyList()); + private void givenNoScopesInTheRepository() { + when(scopeRbacRepo.findAll()).thenReturn(emptyList()); } - private void givenSomeContextsInTheRepository() { - when(loginContextRbacRepo.findAll()).thenReturn(List.of( - HsCredentialsContextRbacEntity.builder() + private void givenSomeScopesInTheRepository() { + when(scopeRbacRepo.findAll()).thenReturn(List.of( + HsProfileScopeRbacEntity.builder() .uuid(UUID.randomUUID()) .type("HSADMIN") .qualifier("prod") .publicAccess(true) .onlyForNaturalPersons(true) .build(), - HsCredentialsContextRbacEntity.builder() + HsProfileScopeRbacEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("public") .publicAccess(true) .onlyForNaturalPersons(false) .build(), - HsCredentialsContextRbacEntity.builder() + HsProfileScopeRbacEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("internal") diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntityUnitTest.java similarity index 73% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntityUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntityUnitTest.java index 4223d277..e366bd8f 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacEntityUnitTest.java @@ -6,11 +6,11 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; -class HsCredentialsContextRbacEntityUnitTest { +class HsProfileScopeRbacEntityUnitTest { @Test void toShortStringContainsJustTypeAndQualifier() { - final var entity = HsCredentialsContextRbacEntity.builder() + final var entity = HsProfileScopeRbacEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("prod") @@ -21,12 +21,12 @@ class HsCredentialsContextRbacEntityUnitTest { @Test void toStringContainsAllNonNullFields() { - final var entity = HsCredentialsContextRbacEntity.builder() + final var entity = HsProfileScopeRbacEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("prod") .publicAccess(true) .build(); - assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString()); + assertEquals("scope(SSH:prod:PUBLIC)", entity.toString()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepositoryIntegrationTest.java similarity index 70% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepositoryIntegrationTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepositoryIntegrationTest.java index 0126c2ad..e2364c16 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRbacRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRbacRepositoryIntegrationTest.java @@ -25,11 +25,11 @@ import static org.assertj.core.api.Assertions.catchThrowable; @Tag("generalIntegrationTest") @Import({ Context.class, JpaAttempt.class }) @Transactional -class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest { +class HsProfileScopeRbacRepositoryIntegrationTest extends ContextBasedTest { - // existing UUIDs from test data (Liquibase changeset 310-login-credentials-test-data.sql) - private static final UUID TEST_CONTEXT_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); - private static final UUID TEST_CONTEXT_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); + // existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql) + private static final UUID TEST_SCOPE_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID TEST_SCOPE_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net"; private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org"; @@ -38,21 +38,21 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest HttpServletRequest request; @Autowired - private HsCredentialsContextRbacRepository loginContextRepository; + private HsProfileScopeRbacRepository scopesRepository; @Test void shouldFindAllByNormalUserUsingTestData() { context(TEST_USER_SUBJECT_NAME); // when - final var allContexts = loginContextRepository.findAll(); + final var allScopes = scopesRepository.findAll(); // then - assertThat(allContexts) + assertThat(allScopes) .isNotNull() .hasSizeGreaterThanOrEqualTo(1) // Expect at least the 1 public context from assumed test data - .extracting(HsCredentialsContext::getUuid) - .contains(TEST_CONTEXT_HSADMIN_PROD_UUID); + .extracting(HsProfileScope::getUuid) + .contains(TEST_SCOPE_HSADMIN_PROD_UUID); } @Test @@ -60,12 +60,12 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest context(SUPERUSER_ALEX_SUBJECT_NAME); // when - final var allContexts = loginContextRepository.findAll(); + final var allScopes = scopesRepository.findAll(); // then - assertThat(allContexts) + assertThat(allScopes) .isNotNull() - .hasSizeGreaterThanOrEqualTo(3); // Expect at least the 1 public context from assumed test data + .hasSizeGreaterThanOrEqualTo(3); // Expect at least the 1 public scope from assumed test data } @Test @@ -73,11 +73,11 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest context(TEST_USER_SUBJECT_NAME); // when - final var foundEntityOptional = loginContextRepository.findByUuid(TEST_CONTEXT_HSADMIN_PROD_UUID); + final var foundEntityOptional = scopesRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID); // then assertThat(foundEntityOptional).isPresent(); - assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)"); + assertThat(foundEntityOptional).map(Object::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)"); } @Test @@ -85,11 +85,11 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest context(SUPERUSER_ALEX_SUBJECT_NAME); // when - final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier("SSH", "internal"); + final var foundEntityOptional = scopesRepository.findByTypeAndQualifier("SSH", "internal"); // then assertThat(foundEntityOptional).isPresent(); - assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal:NP-ONLY:INTERNAL)"); + assertThat(foundEntityOptional).map(Object::toString).contains("scope(SSH:internal:NP-ONLY:INTERNAL)"); } @Test @@ -100,7 +100,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest final var nonExistentQualifier = "non-existent-qualifier"; // when - final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier( + final var foundEntityOptional = scopesRepository.findByTypeAndQualifier( "HSADMIN", nonExistentQualifier); // then @@ -108,19 +108,19 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest } @Test - void shouldSaveNewLoginContext() { + void shouldSaveNewScope() { context(SUPERUSER_ALEX_SUBJECT_NAME); // given final var newQualifier = "test@example.social"; final var newType = "MASTODON"; - final var newContext = HsCredentialsContextRbacEntity.builder() + final var newScope = HsProfileScopeRbacEntity.builder() .type(newType) .qualifier(newQualifier) .build(); // when - final var savedEntity = loginContextRepository.save(newContext); + final var savedEntity = scopesRepository.save(newScope); em.flush(); em.clear(); @@ -131,7 +131,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest // Fetch again using the generated UUID to confirm persistence context(SUPERUSER_ALEX_SUBJECT_NAME); // Re-set context if needed after clear - final var foundEntityOptional = loginContextRepository.findByUuid(generatedUuid); + final var foundEntityOptional = scopesRepository.findByUuid(generatedUuid); assertThat(foundEntityOptional).isPresent(); final var foundEntity = foundEntityOptional.get(); assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid); @@ -140,21 +140,21 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest } @Test - void shouldPreventUpdateOfExistingLoginContext() { + void shouldPreventUpdateOfExistingScope() { context(SUPERUSER_ALEX_SUBJECT_NAME); // given an existing entity from test data - final var entityToUpdateOptional = loginContextRepository.findByUuid(TEST_CONTEXT_MATRIX_INTERNAL_UUID); + final var entityToUpdateOptional = scopesRepository.findByUuid(TEST_SCOPE_MATRIX_INTERNAL_UUID); assertThat(entityToUpdateOptional) - .withFailMessage("Could not find existing LoginContext with UUID %s. Ensure test data exists.", - TEST_CONTEXT_MATRIX_INTERNAL_UUID) + .withFailMessage("Could not find existing scope with UUID %s. Ensure test data exists.", + TEST_SCOPE_MATRIX_INTERNAL_UUID) .isPresent(); final var entityToUpdate = entityToUpdateOptional.get(); // when entityToUpdate.setQualifier("updated"); final var exception = catchThrowable( () -> { - loginContextRepository.save(entityToUpdate); + scopesRepository.save(entityToUpdate); em.flush(); }); @@ -162,6 +162,6 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest assertThat(exception) .isInstanceOf(PersistenceException.class) .hasCauseInstanceOf(PSQLException.class) - .hasMessageContaining("ERROR: Updates to hs_accounts.context are not allowed."); + .hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed."); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntityUnitTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntityUnitTest.java similarity index 73% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntityUnitTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntityUnitTest.java index 54ce54fb..40971ff3 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealEntityUnitTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealEntityUnitTest.java @@ -6,11 +6,11 @@ import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; -class HsCredentialsContextRealEntityUnitTest { +class HsProfileScopeRealEntityUnitTest { @Test void toShortStringContainsJustTypeAndQualifier() { - final var entity = HsCredentialsContextRealEntity.builder() + final var entity = HsProfileScopeRealEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("prod") @@ -21,12 +21,12 @@ class HsCredentialsContextRealEntityUnitTest { @Test void toStringContainsAllNonNullFields() { - final var entity = HsCredentialsContextRealEntity.builder() + final var entity = HsProfileScopeRealEntity.builder() .uuid(UUID.randomUUID()) .type("SSH") .qualifier("prod") .publicAccess(true) .build(); - assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString()); + assertEquals("scope(SSH:prod:PUBLIC)", entity.toString()); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepositoryIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepositoryIntegrationTest.java similarity index 69% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepositoryIntegrationTest.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepositoryIntegrationTest.java index df140c16..4eefe012 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsCredentialsContextRealRepositoryIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/HsProfileScopeRealRepositoryIntegrationTest.java @@ -25,12 +25,12 @@ import static org.assertj.core.api.Assertions.catchThrowable; @ActiveProfiles("test") @Tag("generalIntegrationTest") @Import({ Context.class, JpaAttempt.class }) -class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest { +class HsProfileScopeRealRepositoryIntegrationTest extends ContextBasedTest { - // existing UUIDs from test data (Liquibase changeset 310-login-credentials-test-data.sql) - private static final UUID TEST_CONTEXT_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); - private static final UUID TEST_CONTEXT_SSH_INTERNAL_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); - private static final UUID TEST_CONTEXT_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); + // existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql) + private static final UUID TEST_SCOPE_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111"); + private static final UUID TEST_SCOPE_SSH_INTERNAL_UUID = UUID.fromString("22222222-2222-2222-2222-222222222222"); + private static final UUID TEST_SCOPE_MATRIX_INTERNAL_UUID = UUID.fromString("33333333-3333-3333-3333-333333333333"); private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net"; private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org"; @@ -39,12 +39,12 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest HttpServletRequest request; @Autowired - private HsCredentialsContextRealRepository loginContextRepository; + private HsProfileScopeRealRepository scopeRepository; @Test public void historizationIsAvailable() { // given - final String nativeQuerySql = "select * from hs_accounts.context_hv"; + final String nativeQuerySql = "select * from hs_accounts.scope_hv"; // when historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant())); @@ -53,7 +53,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest // then assertThat(rowsBefore) - .as("hs_accounts.context_hv only contain no rows for a timestamp before test data creation") + .as("hs_accounts.scope_hv only contain no rows for a timestamp before test data creation") .hasSize(0); // and when @@ -63,7 +63,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest // then assertThat(rowsAfter) - .as("hs_accounts.context_hv should now contain the test-data rows for the current timestamp") + .as("hs_accounts.scope_hv should now contain the test-data rows for the current timestamp") .hasSize(7); } @@ -72,14 +72,14 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest context(TEST_USER_SUBJECT_NAME); // when - final var allContexts = loginContextRepository.findAll(); + final var allScopes = scopeRepository.findAll(); // then - assertThat(allContexts) + assertThat(allScopes) .isNotNull() .hasSizeGreaterThanOrEqualTo(3) // Expect at least the 3 from assumed test data - .extracting(HsCredentialsContext::getUuid) - .contains(TEST_CONTEXT_HSADMIN_PROD_UUID, TEST_CONTEXT_SSH_INTERNAL_UUID, TEST_CONTEXT_MATRIX_INTERNAL_UUID); + .extracting(HsProfileScope::getUuid) + .contains(TEST_SCOPE_HSADMIN_PROD_UUID, TEST_SCOPE_SSH_INTERNAL_UUID, TEST_SCOPE_MATRIX_INTERNAL_UUID); } @Test @@ -87,11 +87,11 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest context(TEST_USER_SUBJECT_NAME); // when - final var foundEntityOptional = loginContextRepository.findByUuid(TEST_CONTEXT_HSADMIN_PROD_UUID); + final var foundEntityOptional = scopeRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID); // then assertThat(foundEntityOptional).isPresent(); - assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)"); + assertThat(foundEntityOptional).map(Object::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)"); } @Test @@ -99,11 +99,11 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest context(TEST_USER_SUBJECT_NAME); // when - final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier("SSH", "internal"); + final var foundEntityOptional = scopeRepository.findByTypeAndQualifier("SSH", "internal"); // then assertThat(foundEntityOptional).isPresent(); - assertThat(foundEntityOptional).map(Object::toString).contains("loginContext(SSH:internal:NP-ONLY:INTERNAL)"); + assertThat(foundEntityOptional).map(Object::toString).contains("scope(SSH:internal:NP-ONLY:INTERNAL)"); } @Test @@ -114,7 +114,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest final var nonExistentQualifier = "non-existent-qualifier"; // when - final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier( + final var foundEntityOptional = scopeRepository.findByTypeAndQualifier( "HSADMIN", nonExistentQualifier); // then @@ -122,19 +122,19 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest } @Test - void shouldSaveNewLoginContext() { + void shouldSaveNewScope() { context(SUPERUSER_ALEX_SUBJECT_NAME); // given final var newQualifier = "test@example.social"; final var newType = "MASTODON"; - final var newContext = HsCredentialsContextRealEntity.builder() + final var newScope = HsProfileScopeRealEntity.builder() .type(newType) .qualifier(newQualifier) .build(); // when - final var savedEntity = loginContextRepository.save(newContext); + final var savedEntity = scopeRepository.save(newScope); em.flush(); em.clear(); @@ -145,7 +145,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest // Fetch again using the generated UUID to confirm persistence context(TEST_USER_SUBJECT_NAME); // Re-set context if needed after clear - final var foundEntityOptional = loginContextRepository.findByUuid(generatedUuid); + final var foundEntityOptional = scopeRepository.findByUuid(generatedUuid); assertThat(foundEntityOptional).isPresent(); final var foundEntity = foundEntityOptional.get(); assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid); @@ -154,21 +154,21 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest } @Test - void shouldPreventUpdateOfExistingLoginContext() { + void shouldPreventUpdateOfExistingScope() { context(TEST_USER_SUBJECT_NAME); // given an existing entity from test data - final var entityToUpdateOptional = loginContextRepository.findByUuid(TEST_CONTEXT_MATRIX_INTERNAL_UUID); + final var entityToUpdateOptional = scopeRepository.findByUuid(TEST_SCOPE_MATRIX_INTERNAL_UUID); assertThat(entityToUpdateOptional) - .withFailMessage("Could not find existing LoginContext with UUID %s. Ensure test data exists.", - TEST_CONTEXT_MATRIX_INTERNAL_UUID) + .withFailMessage("Could not find existing Scope with UUID %s. Ensure test data exists.", + TEST_SCOPE_MATRIX_INTERNAL_UUID) .isPresent(); final var entityToUpdate = entityToUpdateOptional.get(); // when entityToUpdate.setQualifier("updated"); final var exception = catchThrowable( () -> { - loginContextRepository.save(entityToUpdate); + scopeRepository.save(entityToUpdate); em.flush(); }); @@ -176,6 +176,6 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest assertThat(exception) .isInstanceOf(PersistenceException.class) .hasCauseInstanceOf(PSQLException.class) - .hasMessageContaining("ERROR: Updates to hs_accounts.context are not allowed."); + .hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed."); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseCredentialsUseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseCredentialsUseCase.java deleted file mode 100644 index 022af18b..00000000 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseCredentialsUseCase.java +++ /dev/null @@ -1,38 +0,0 @@ -package net.hostsharing.hsadminng.hs.accounts.scenarios; - -import lombok.SneakyThrows; -import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource; -import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; -import net.hostsharing.hsadminng.hs.scenarios.UseCase; -import org.apache.commons.lang3.tuple.Pair; - -import java.util.Arrays; - -import static io.restassured.http.ContentType.JSON; -import static org.springframework.http.HttpStatus.OK; - -public abstract class BaseCredentialsUseCase> extends UseCase { - - public BaseCredentialsUseCase(final ScenarioTest testSuite) { - super(testSuite); - } - - @SneakyThrows - protected ContextResource[] fetchContextResourcesByDescriptorPairs(final String descriptPairsVarName) { - final var requestedContexts = ScenarioTest.getTypedVariable("contexts", Pair[].class); - final var existingContextsJson = withTitle("Fetch Available Account Contexts", () -> - httpGet("/api/hs/accounts/contexts").expecting(OK).expecting(JSON) - ).getResponse().body(); - final var existingContexts = objectMapper.readValue(existingContextsJson, ContextResource[].class); - return Arrays.stream(requestedContexts) - .map(pair -> Arrays.stream(existingContexts) - .filter(context -> context.getType().equals(pair.getLeft()) - && context.getQualifier().equals(pair.getRight())) - .findFirst() - .orElseThrow(() -> new IllegalStateException( - "No matching context found for type=" + pair.getLeft() - + " and qualifier=" + pair.getRight())) - ) - .toArray(ContextResource[]::new); - } -} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseProfileUseCase.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseProfileUseCase.java new file mode 100644 index 00000000..6595a623 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/BaseProfileUseCase.java @@ -0,0 +1,38 @@ +package net.hostsharing.hsadminng.hs.accounts.scenarios; + +import lombok.SneakyThrows; +import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource; +import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest; +import net.hostsharing.hsadminng.hs.scenarios.UseCase; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.Arrays; + +import static io.restassured.http.ContentType.JSON; +import static org.springframework.http.HttpStatus.OK; + +public abstract class BaseProfileUseCase> extends UseCase { + + public BaseProfileUseCase(final ScenarioTest testSuite) { + super(testSuite); + } + + @SneakyThrows + protected ScopeResource[] fetchScopeResourcesByDescriptorPairs(final String descriptPairsVarName) { + final var requestedScopes = ScenarioTest.getTypedVariable("scopes", Pair[].class); + final var existingScopesJson = withTitle("Fetch Available Account Scopes", () -> + httpGet("/api/hs/accounts/scopes").expecting(OK).expecting(JSON) + ).getResponse().body(); + final var existingScopes = objectMapper.readValue(existingScopesJson, ScopeResource[].class); + return Arrays.stream(requestedScopes) + .map(pair -> Arrays.stream(existingScopes) + .filter(scope -> scope.getType().equals(pair.getLeft()) + && scope.getQualifier().equals(pair.getRight())) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "No matching scope found for type=" + pair.getLeft() + + " and qualifier=" + pair.getRight())) + ) + .toArray(ScopeResource[]::new); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateCredentials.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateProfile.java similarity index 64% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateCredentials.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateProfile.java index 0e7191c8..5e326014 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateCredentials.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CreateProfile.java @@ -8,12 +8,12 @@ import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.OK; -public class CreateCredentials extends BaseCredentialsUseCase { +public class CreateProfile extends BaseProfileUseCase { - public CreateCredentials(final ScenarioTest testSuite) { + public CreateProfile(final ScenarioTest testSuite) { super(testSuite); - introduction("A set of credentials contains the login data for an RBAC subject."); + introduction("A set of profile contains the login data for an RBAC subject."); } @Override @@ -26,12 +26,12 @@ public class CreateCredentials extends BaseCredentialsUseCase "In real situations we have more precise measures to find the related person." ); - given("resolvedContexts", - fetchContextResourcesByDescriptorPairs("contexts") + given("resolvedScopes", + fetchScopeResourcesByDescriptorPairs("scopes") ); - return obtain("newCredentials", () -> - httpPost("/api/hs/accounts/credentials", usingJsonBody(""" + return obtain("newProfile", () -> + httpPost("/api/hs/accounts/profiles", usingJsonBody(""" { "person.uuid": ${Person: %{personGivenName} %{personFamilyName}}, "nickname": ${nickname}, @@ -40,10 +40,9 @@ public class CreateCredentials extends BaseCredentialsUseCase "emailAddress": ${emailAddress}, "phonePassword": ${phonePassword}, "smsNumber": ${smsNumber}, - "onboardingToken": ${onboardingToken}, "globalUid": %{globalUid}, "globalGid": %{globalGid}, - "contexts": @{resolvedContexts} + "scopes": @{resolvedScopes} } """)) .expecting(HttpStatus.CREATED).expecting(ContentType.JSON) @@ -51,16 +50,15 @@ public class CreateCredentials extends BaseCredentialsUseCase } @Override - protected void verify(final UseCase.HttpResponse response) { + protected void verify(final UseCase.HttpResponse response) { verify( - "Verify the New Credentials", - () -> httpGet("/api/hs/accounts/credentials/%{newCredentials}") + "Verify the new Profile", + () -> httpGet("/api/hs/accounts/profiles/%{newProfile}") .expecting(OK).expecting(JSON), - path("uuid").contains("%{newCredentials}"), + path("uuid").contains("%{newProfile}"), path("nickname").contains("%{nickname}"), path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"), - path("totpSecrets").contains("@{totpSecrets}"), - path("onboardingToken").contains("%{onboardingToken}") + path("totpSecrets").contains("@{totpSecrets}") ); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CredentialsScenarioTests.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/ProfileScenarioTests.java similarity index 80% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CredentialsScenarioTests.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/ProfileScenarioTests.java index 5cfd7665..673508fd 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/CredentialsScenarioTests.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/ProfileScenarioTests.java @@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.junit.jupiter.api.TestMethodOrder; -class CredentialsScenarioTests extends ScenarioTest { +class ProfileScenarioTests extends ScenarioTest { @SneakyThrows @BeforeEach @@ -61,19 +61,19 @@ class CredentialsScenarioTests extends ScenarioTest { @Nested @Order(30) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) - class CredentialScenarios { + class ProfileScenarios { @Test @Order(1010) - @Produces(explicitly = "Credentials@hsadmin: firby-susan", implicitly = { "Person: Susan Firby" }) - void shouldCreateInitialCredentialsForExistingNaturalPerson() { - new CreateCredentials(scenarioTest) + @Produces(explicitly = "Profile: firby-susan", implicitly = { "Person: Susan Firby" }) + void shouldCreateInitialProfileForExistingNaturalPerson() { + new CreateProfile(scenarioTest) // to find a specific existing person .given("personFamilyName", "Firby") .given("personGivenName", "Susan") // a login name, to be stored in the new RBAC subject .given("nickname", "firby-susan") - // initial credentials + // initial profile .given("active", true) .given("totpSecrets", Array.of("initialSecret")) .given("emailAddress", "susan.firby@example.com") @@ -82,29 +82,28 @@ class CredentialsScenarioTests extends ScenarioTest { .given("globalUid", 21011) .given("globalGid", 21011) .given( - "contexts", Array.of( + "scopes", Array.of( Pair.of("HSADMIN", "prod") )) - .given("onboardingToken", "fake-unboarding-token") .doRun() .keep(); } @Test @Order(1020) - @Requires("Credentials@hsadmin: firby-susan") - void shouldUpdateCredentials() { - new UpdateCredentials(scenarioTest) - // the credentials to update - .given("credentialsUuid", "%{Credentials@hsadmin: firby-susan}") - // updated credentials + @Requires("Profile: firby-susan") + void shouldUpdateProfile() { + new UpdateProfile(scenarioTest) + // the profile to update + .given("profileUuid", "%{Profile: firby-susan}") + // updated profile .given("active", false) .given("totpSecrets", Array.of("initialSecret", "additionalSecret")) .given("emailAddress", "susan.firby@example.org") .given("phonePassword", "securePass987") .given("smsNumber", "+49987654321") .given( - "contexts", Array.of( + "scopes", Array.of( Pair.of("HSADMIN", "prod"), Pair.of("SSH", "internal") )) diff --git a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateCredentials.java b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateProfile.java similarity index 60% rename from src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateCredentials.java rename to src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateProfile.java index 4714bc1e..17cab890 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateCredentials.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/accounts/scenarios/UpdateProfile.java @@ -8,30 +8,30 @@ import org.springframework.http.HttpStatus; import static io.restassured.http.ContentType.JSON; import static org.springframework.http.HttpStatus.OK; -public class UpdateCredentials extends BaseCredentialsUseCase { +public class UpdateProfile extends BaseProfileUseCase { - public UpdateCredentials(final ScenarioTest testSuite) { + public UpdateProfile(final ScenarioTest testSuite) { super(testSuite); - introduction("A set of credentials contains the login data for an RBAC subject."); + introduction("A set of profile contains the login data for an RBAC subject."); } @Override protected HttpResponse run() { - given("resolvedContexts", - fetchContextResourcesByDescriptorPairs("contexts") + given("resolvedScopes", + fetchScopeResourcesByDescriptorPairs("scopes") ); - withTitle("Patch the Changes to the existing Credentials", () -> - httpPatch("/api/hs/accounts/credentials/%{credentialsUuid}", usingJsonBody(""" + withTitle("Patch the Changes to the existing Profile", () -> + httpPatch("/api/hs/accounts/profiles/%{profileUuid}", usingJsonBody(""" { "active": %{active}, "totpSecrets": @{totpSecrets}, "emailAddress": ${emailAddress}, "phonePassword": ${phonePassword}, "smsNumber": ${smsNumber}, - "contexts": @{resolvedContexts} + "scopes": @{resolvedScopes} } """)) .reportWithResponse().expecting(HttpStatus.OK).expecting(ContentType.JSON) @@ -43,12 +43,12 @@ public class UpdateCredentials extends BaseCredentialsUseCase } @Override - protected void verify(final UseCase.HttpResponse response) { + protected void verify(final UseCase.HttpResponse response) { verify( - "Verify the Patched Credentials", - () -> httpGet("/api/hs/accounts/credentials/%{credentialsUuid}") + "Verify the Patched Profile", + () -> httpGet("/api/hs/accounts/profiles/%{profileUuid}") .expecting(OK).expecting(JSON), - path("uuid").contains("%{newCredentials}"), + path("uuid").contains("%{newProfile}"), path("nickname").contains("%{nickname}"), path("totpSecrets").contains("%{totpSecrets}") ); diff --git a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java index ad3496c2..82a46443 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/office/bankaccount/HsOfficeBankAccountControllerAcceptanceTest.java @@ -7,7 +7,6 @@ import net.hostsharing.hsadminng.rbac.context.Context; import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup; import net.hostsharing.hsadminng.rbac.test.JpaAttempt; import org.apache.commons.lang3.RandomStringUtils; -import org.json.JSONException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -31,12 +30,12 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; -@Transactional @Tag("officeIntegrationTest") @SpringBootTest( webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = HsadminNgApplication.class) @ActiveProfiles("fake-jwt") +@Transactional class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup { @LocalServerPort @@ -58,7 +57,7 @@ class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCl class GetListOfBankAccounts { @Test - void globalAdmin_withoutAssumedRoles_canViewAllBankAccounts_ifNoCriteriaGiven() throws JSONException { + void globalAdmin_withoutAssumedRoles_canViewAllBankAccounts_ifNoCriteriaGiven() { RestAssured // @formatter:off .given()