rename Credentials->Profile + Context->Scope (#202)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/202 Reviewed-by: Timotheus Pokorra <timotheus.pokorra@hostsharing.net>
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
|
||||||
-24
@@ -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<HsCredentialsContextRbacEntity, UUID> {
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findAll")
|
|
||||||
List<HsCredentialsContextRbacEntity> findAll();
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findByUuid")
|
|
||||||
Optional<HsCredentialsContextRbacEntity> findByUuid(final UUID id);
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findByTypeAndQualifier")
|
|
||||||
Optional<HsCredentialsContextRbacEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.save")
|
|
||||||
HsCredentialsContextRbacEntity save(final HsCredentialsContextRbacEntity entity);
|
|
||||||
}
|
|
||||||
-24
@@ -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<HsCredentialsContextRealEntity, UUID> {
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findAll")
|
|
||||||
List<HsCredentialsContextRealEntity> findAll();
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findByUuid")
|
|
||||||
Optional<HsCredentialsContextRealEntity> findByUuid(final UUID id);
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.findByTypeAndQualifier")
|
|
||||||
Optional<HsCredentialsContextRealEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
|
|
||||||
|
|
||||||
@Timed("app.login.context.repo.save")
|
|
||||||
HsCredentialsContextRealEntity save(final HsCredentialsContextRealEntity entity);
|
|
||||||
}
|
|
||||||
@@ -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<HsCredentialsEntity, UUID> {
|
|
||||||
|
|
||||||
@Timed("app.login.credentials.repo.findByUuid")
|
|
||||||
Optional<HsCredentialsEntity> findByUuid(final UUID uuid);
|
|
||||||
|
|
||||||
@Timed("app.login.credentials.repo.findByPerson")
|
|
||||||
List<HsCredentialsEntity> 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<HsCredentialsEntity> findByCurrentSubject();
|
|
||||||
|
|
||||||
@Timed("app.login.credentials.repo.save")
|
|
||||||
HsCredentialsEntity save(final HsCredentialsEntity entity);
|
|
||||||
}
|
|
||||||
+96
-117
@@ -1,7 +1,5 @@
|
|||||||
package net.hostsharing.hsadminng.hs.accounts;
|
package net.hostsharing.hsadminng.hs.accounts;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
@@ -10,15 +8,15 @@ import java.util.stream.Collectors;
|
|||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import lombok.val;
|
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.CurrentLoginUserResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource;
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
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.api.ProfileApi;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsInsertResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileInsertResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfilePatchResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ProfileResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
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.persistence.EntityNotFoundException;
|
||||||
import jakarta.validation.ValidationException;
|
import jakarta.validation.ValidationException;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
|
||||||
import static java.util.Optional.of;
|
import static java.util.Optional.of;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@PreAuthorize("isAuthenticated()")
|
@PreAuthorize("isAuthenticated()")
|
||||||
@SecurityRequirement(name = "bearerAuth")
|
@SecurityRequirement(name = "bearerAuth")
|
||||||
public class HsCredentialsController implements CredentialsApi {
|
public class HsProfileController implements ProfileApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@@ -58,7 +55,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
private RbacSubjectRepository subjectRepo;
|
private RbacSubjectRepository subjectRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private CredentialContextResourceToEntityMapper contextMapper;
|
private ScopeResourceToEntityMapper scopeMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MessageTranslator messageTranslator;
|
private MessageTranslator messageTranslator;
|
||||||
@@ -67,113 +64,113 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
private HsOfficePersonRealRepository realPersonRepo;
|
private HsOfficePersonRealRepository realPersonRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepo;
|
private HsProfileRepository profileRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private RbacSubjectRepository rbacSubjectRepo;
|
private RbacSubjectRepository rbacSubjectRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
@Timed("app.accounts.profile.getSingleProfileByUuid")
|
||||||
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(final UUID credentialsUuid) {
|
public ResponseEntity<ProfileResource> getSingleProfileByUuid(final UUID profileUuid) {
|
||||||
|
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
|
||||||
val credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
val profileEntity = profileRepo.findByUuid(profileUuid);
|
||||||
if (credentialsEntity.isEmpty()) {
|
if (profileEntity.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
val result = mapper.map(
|
val result = mapper.map(
|
||||||
credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
profileEntity.get(), ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
|
@Timed("app.accounts.profile.getListOfProfileByPersonUuid")
|
||||||
public ResponseEntity<List<CredentialsResource>> getListOfCredentials(
|
public ResponseEntity<List<ProfileResource>> getListOfProfile(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final UUID personUuid
|
final UUID personUuid
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
val credentials = personUuid == null
|
val profile = personUuid == null
|
||||||
? credentialsRepo.findByCurrentSubject()
|
? profileRepo.findByCurrentSubject()
|
||||||
: findByPersonUuid(personUuid);
|
: findByPersonUuid(personUuid);
|
||||||
val result = mapper.mapList(
|
val result = mapper.mapList(
|
||||||
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
profile, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.postNewCredentials")
|
@Timed("app.accounts.profile.postNewProfile")
|
||||||
public ResponseEntity<CredentialsResource> postNewCredentials(
|
public ResponseEntity<ProfileResource> postNewProfile(
|
||||||
final CredentialsInsertResource body
|
final ProfileInsertResource body
|
||||||
) {
|
) {
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
|
||||||
// first create and save the subject to get its UUID
|
// first create and save the subject to get its UUID
|
||||||
val newlySavedSubject = createSubject(body.getNickname());
|
val newlySavedSubject = createSubject(body.getNickname());
|
||||||
|
|
||||||
// afterward, create and save the credentials entity with the subject's UUID
|
// afterward, create and save the profile entity with the subject's UUID
|
||||||
val newCredentialsEntity = mapper.map(
|
val newProfileEntity = mapper.map(
|
||||||
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
body, HsProfileEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
validateOnCreate(newCredentialsEntity);
|
validateOnCreate(newProfileEntity);
|
||||||
|
|
||||||
// switch to the new subject to get access to its own subject RBAC object
|
// 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);
|
context.define("activate newly created self-service subject", null, body.getNickname(), null);
|
||||||
newCredentialsEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject
|
newProfileEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject
|
||||||
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
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 =
|
val uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/accounts/credentials/{id}")
|
.path("/api/hs/accounts/profiles/{id}")
|
||||||
.buildAndExpand(newCredentialsEntity.getUuid())
|
.buildAndExpand(newProfileEntity.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
val newCredentialsResource = mapper.map(
|
val newProfileResource = mapper.map(
|
||||||
newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
newProfileEntity, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.created(uri).body(newCredentialsResource);
|
return ResponseEntity.created(uri).body(newProfileResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
@Timed("app.accounts.profile.deleteProfileByUuid")
|
||||||
public ResponseEntity<Void> deleteCredentialsByUuid(final UUID credentialsUuid) {
|
public ResponseEntity<Void> deleteProfileByUuid(final UUID profileUuid) {
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
val credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
val profileEntity = em.getReference(HsProfileEntity.class, profileUuid);
|
||||||
credentialsEntity.getLoginContexts().clear();
|
profileEntity.getScopes().clear();
|
||||||
validateOnDelete(credentialsEntity);
|
validateOnDelete(profileEntity);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.remove(credentialsEntity);
|
em.remove(profileEntity);
|
||||||
em.remove(credentialsEntity.getSubject());
|
em.remove(profileEntity.getSubject());
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.patchCredentials")
|
@Timed("app.accounts.profile.patchProfile")
|
||||||
public ResponseEntity<CredentialsResource> patchCredentials(
|
public ResponseEntity<ProfileResource> patchProfile(
|
||||||
final UUID credentialsUuid,
|
final UUID profileUuid,
|
||||||
final CredentialsPatchResource body
|
final ProfilePatchResource body
|
||||||
) {
|
) {
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
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);
|
validateOnUpdate(current);
|
||||||
|
|
||||||
val saved = credentialsRepo.save(current);
|
val saved = profileRepo.save(current);
|
||||||
val mapped = mapper.map(
|
val mapped = mapper.map(
|
||||||
saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
saved, ProfileResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.getCurrentLoginUser")
|
@Timed("app.accounts.profile.getCurrentLoginUser")
|
||||||
public ResponseEntity<CurrentLoginUserResource> getCurrentLoginUser() {
|
public ResponseEntity<CurrentLoginUserResource> getCurrentLoginUser() {
|
||||||
|
|
||||||
// define a context without assumed roles, otherwise we cannot access the subject anymore
|
// 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
|
// fetch the data
|
||||||
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
val currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
val currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
||||||
val person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
|
val person = profileRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
|
||||||
|
|
||||||
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
||||||
|
|
||||||
@@ -191,46 +188,30 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private void validateOnCreate(final HsProfileEntity newProfileEntity) {
|
||||||
@Transactional
|
validateReferencedPersonToBeRepresentedByLoginUserPerson(newProfileEntity);
|
||||||
@Timed("app.credentials.credentials.credentialsUsed")
|
validateNormalUsersOnlyAccessPublicScopes(newProfileEntity);
|
||||||
public ResponseEntity<CredentialsResource> credentialsUsed(final UUID credentialsUuid) {
|
validateNaturalPersonRequirementOfScopes(newProfileEntity);
|
||||||
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 HsCredentialsEntity newCredentialsEntity) {
|
private void validateOnUpdate(final HsProfileEntity current) {
|
||||||
validateReferencedPersonToBeRepresentedByLoginUserPerson(newCredentialsEntity);
|
validateNormalUsersOnlyAccessPublicScopes(current);
|
||||||
validateNormalUsersOnlyAccessPublicContexts(newCredentialsEntity);
|
validateNaturalPersonRequirementOfScopes(current);
|
||||||
validateNaturalPersonRequirementOfContexts(newCredentialsEntity);
|
validateOwnHsadminProfileMustNotBeRemoved(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateOnUpdate(final HsCredentialsEntity current) {
|
private void validateOnDelete(final HsProfileEntity profileEntity) {
|
||||||
validateNormalUsersOnlyAccessPublicContexts(current);
|
validateOwnHsadminProfileMustNotBeRemoved(profileEntity);
|
||||||
validateNaturalPersonRequirementOfContexts(current);
|
|
||||||
validateOwnHsadminCredentialsMustNotBeRemoved(current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateOnDelete(final HsCredentialsEntity credentialsEntity) {
|
private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsProfileEntity newProfileEntity) {
|
||||||
validateOwnHsadminCredentialsMustNotBeRemoved(credentialsEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsCredentialsEntity newCredentialsEntity) {
|
|
||||||
if (context.isGlobalAdmin()) {
|
if (context.isGlobalAdmin()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
val referredPersonUuid = newCredentialsEntity.getPerson().getUuid();
|
val referredPersonUuid = newProfileEntity.getPerson().getUuid();
|
||||||
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
val loginPersonUuid = credentialsRepo.findByUuid(currentSubjectUuid)
|
val loginPersonUuid = profileRepo.findByUuid(currentSubjectUuid)
|
||||||
.map(HsCredentialsEntity::getPerson)
|
.map(HsProfileEntity::getPerson)
|
||||||
.map(HsOfficePerson::getUuid)
|
.map(HsOfficePerson::getUuid)
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
val representedPersonUuids = realPersonRepo.findPersonsRepresentedByPersonWithUuid(loginPersonUuid)
|
val representedPersonUuids = realPersonRepo.findPersonsRepresentedByPersonWithUuid(loginPersonUuid)
|
||||||
@@ -238,58 +219,58 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
if ( !representedPersonUuids.contains(referredPersonUuid)) {
|
if ( !representedPersonUuids.contains(referredPersonUuid)) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
messageTranslator.translate(
|
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));
|
loginPersonUuid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateNormalUsersOnlyAccessPublicContexts(final HsCredentialsEntity newCredentialsEntity) {
|
private void validateNormalUsersOnlyAccessPublicScopes(final HsProfileEntity newProfileEntity) {
|
||||||
val forbiddenContexts = newCredentialsEntity.getLoginContexts().stream()
|
val forbiddenScopes = newProfileEntity.getScopes().stream()
|
||||||
.filter(c -> !c.isPublicAccess() && !context.isGlobalAdmin() )
|
.filter(c -> !c.isPublicAccess() && !context.isGlobalAdmin() )
|
||||||
.toList();
|
.toList();
|
||||||
if (!forbiddenContexts.isEmpty()) {
|
if (!forbiddenScopes.isEmpty()) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.access-denied-for-contexts-{0}",
|
"profile.access-denied-for-scopes-{0}",
|
||||||
toDisplay(forbiddenContexts)
|
toDisplay(forbiddenScopes)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateNaturalPersonRequirementOfContexts(final HsCredentialsEntity newCredentialsEntity) {
|
private void validateNaturalPersonRequirementOfScopes(final HsProfileEntity newProfileEntity) {
|
||||||
if (newCredentialsEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) {
|
if (newProfileEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
val contextsWhichRequireNaturalPerson = newCredentialsEntity.getLoginContexts().stream()
|
val scopesWhichRequireNaturalPerson = newProfileEntity.getScopes().stream()
|
||||||
.filter(HsCredentialsContext::isOnlyForNaturalPersons)
|
.filter(HsProfileScope::isOnlyForNaturalPersons)
|
||||||
.toList();
|
.toList();
|
||||||
if (!contextsWhichRequireNaturalPerson.isEmpty()) {
|
if (!scopesWhichRequireNaturalPerson.isEmpty()) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.context-requires-natural-person-{0}",
|
"profile.scope-requires-natural-person-{0}",
|
||||||
toDisplay(contextsWhichRequireNaturalPerson)
|
toDisplay(scopesWhichRequireNaturalPerson)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateOwnHsadminCredentialsMustNotBeRemoved(final HsCredentialsEntity newCredentialsEntity) {
|
private void validateOwnHsadminProfileMustNotBeRemoved(final HsProfileEntity newProfileEntity) {
|
||||||
if (!newCredentialsEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) {
|
if (!newProfileEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
val hsadminCredentialsContext = newCredentialsEntity.getLoginContexts().stream()
|
val hsadminProfileScope = newProfileEntity.getScopes().stream()
|
||||||
.filter(HsCredentialsContext::isHsadminContext)
|
.filter(HsProfileScope::isHsadminScope)
|
||||||
.toList();
|
.toList();
|
||||||
if (hsadminCredentialsContext.isEmpty()) {
|
if (hsadminProfileScope.isEmpty()) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.own-hsadmin-credentials-must-not-be-removed"
|
"profile.own-hsadmin-profile-must-not-be-removed"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String toDisplay(final List<HsCredentialsContextRealEntity> contextsWhichRequireNaturalPerson) {
|
private static String toDisplay(final List<HsProfileScopeRealEntity> scopesWhichRequireNaturalPerson) {
|
||||||
return contextsWhichRequireNaturalPerson.stream()
|
return scopesWhichRequireNaturalPerson.stream()
|
||||||
.map(HsCredentialsContext::toShortString)
|
.map(HsProfileScope::toShortString)
|
||||||
.sorted()
|
.sorted()
|
||||||
.map(s -> "'" + s + "'")
|
.map(s -> "'" + s + "'")
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
@@ -301,14 +282,14 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return newRbacSubject;
|
return newRbacSubject;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
private List<HsProfileEntity> findByPersonUuid(final UUID personUuid) {
|
||||||
val person = realPersonRepo.findByUuid(personUuid).orElseThrow(
|
val person = realPersonRepo.findByUuid(personUuid).orElseThrow(
|
||||||
() -> new EntityNotFoundException(
|
() -> new EntityNotFoundException(
|
||||||
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", personUuid)
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
final BiConsumer<HsProfileEntity, ProfileResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
ofNullable(entity.getLastUsed()).ifPresent(
|
|
||||||
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
|
|
||||||
of(entity.getSubject()).ifPresent(
|
of(entity.getSubject()).ifPresent(
|
||||||
subject -> resource.setNickname(subject.getName())
|
subject -> resource.setNickname(subject.getName())
|
||||||
);
|
);
|
||||||
@@ -335,25 +314,25 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
resource.setContexts(mapToValidContextResources(entity));
|
resource.setScopes(mapToValidScopeResources(entity));
|
||||||
};
|
};
|
||||||
|
|
||||||
private List<ContextResource> mapToValidContextResources(final HsCredentialsEntity entity) {
|
private List<ScopeResource> mapToValidScopeResources(final HsProfileEntity entity) {
|
||||||
var allContexts = mapper.mapList(entity.getLoginContexts().stream().toList(), ContextResource.class);
|
var allScopes = mapper.mapList(entity.getScopes().stream().toList(), ScopeResource.class);
|
||||||
return allContexts.stream()
|
return allScopes.stream()
|
||||||
.filter(context -> !context.getOnlyForNaturalPersons() ||
|
.filter(scope -> !scope.getOnlyForNaturalPersons() ||
|
||||||
entity.getPerson().getPersonType() == HsOfficePersonType.NATURAL_PERSON)
|
entity.getPerson().getPersonType() == HsOfficePersonType.NATURAL_PERSON)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
final BiConsumer<ProfileInsertResource, HsProfileEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
val person = realPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
val person = realPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
||||||
() -> new EntityNotFoundException(
|
() -> new EntityNotFoundException(
|
||||||
messageTranslator.translate("general.{0}-{1}-not-found-or-not-accessible", "personUuid", resource.getPersonUuid())
|
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);
|
entity.setPerson(person);
|
||||||
};
|
};
|
||||||
+17
-24
@@ -9,7 +9,6 @@ import net.hostsharing.hsadminng.repr.Stringify;
|
|||||||
import net.hostsharing.hsadminng.repr.Stringifyable;
|
import net.hostsharing.hsadminng.repr.Stringifyable;
|
||||||
// import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship
|
// import net.hostsharing.hsadminng.rbac.RbacSubjectEntity; // Assuming RbacSubjectEntity exists for the FK relationship
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -20,20 +19,20 @@ import static jakarta.persistence.CascadeType.REFRESH;
|
|||||||
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
import static net.hostsharing.hsadminng.repr.Stringify.stringify;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(schema = "hs_accounts", name = "credentials")
|
@Table(schema = "hs_accounts", name = "profile")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Stringifyable {
|
public class HsProfileEntity implements BaseEntity<HsProfileEntity>, Stringifyable {
|
||||||
|
|
||||||
protected static Stringify<HsCredentialsEntity> stringify = stringify(HsCredentialsEntity.class, "credentials")
|
protected static Stringify<HsProfileEntity> stringify = stringify(HsProfileEntity.class, "profile")
|
||||||
.withProp(HsCredentialsEntity::isActive)
|
.withProp(HsProfileEntity::isActive)
|
||||||
.withProp(HsCredentialsEntity::getEmailAddress)
|
.withProp(HsProfileEntity::getEmailAddress)
|
||||||
.withProp(HsCredentialsEntity::getTotpSecrets)
|
.withProp(HsProfileEntity::getTotpSecrets)
|
||||||
.withProp(HsCredentialsEntity::getPhonePassword)
|
.withProp(HsProfileEntity::getPhonePassword)
|
||||||
.withProp(HsCredentialsEntity::getSmsNumber)
|
.withProp(HsProfileEntity::getSmsNumber)
|
||||||
.quotedValues(false);
|
.quotedValues(false);
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@@ -46,14 +45,11 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
|
|
||||||
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
@ManyToOne(optional = false, fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "person_uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
|
@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
|
@Version
|
||||||
private int version;
|
private int version;
|
||||||
|
|
||||||
@Column
|
|
||||||
private LocalDateTime lastUsed;
|
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private boolean active;
|
private boolean active;
|
||||||
|
|
||||||
@@ -63,9 +59,6 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
@Column
|
@Column
|
||||||
private Integer globalGid;
|
private Integer globalGid;
|
||||||
|
|
||||||
@Column
|
|
||||||
private String onboardingToken;
|
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
private List<String> totpSecrets;
|
private List<String> totpSecrets;
|
||||||
|
|
||||||
@@ -80,17 +73,17 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
|
|
||||||
@OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH })
|
@OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH })
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "context_mapping", schema = "hs_accounts",
|
name = "scope_mapping", schema = "hs_accounts",
|
||||||
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
|
joinColumns = @JoinColumn(name = "profile_uuid", referencedColumnName = "uuid"),
|
||||||
inverseJoinColumns = @JoinColumn(name = "context_uuid", referencedColumnName = "uuid")
|
inverseJoinColumns = @JoinColumn(name = "scope_uuid", referencedColumnName = "uuid")
|
||||||
)
|
)
|
||||||
private Set<HsCredentialsContextRealEntity> loginContexts;
|
private Set<HsProfileScopeRealEntity> scopes;
|
||||||
|
|
||||||
public Set<HsCredentialsContextRealEntity> getLoginContexts() {
|
public Set<HsProfileScopeRealEntity> getScopes() {
|
||||||
if ( loginContexts == null ) {
|
if ( scopes == null ) {
|
||||||
loginContexts = new HashSet<>();
|
scopes = new HashSet<>();
|
||||||
}
|
}
|
||||||
return loginContexts;
|
return scopes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSubject(final RbacSubjectEntity subject) {
|
public void setSubject(final RbacSubjectEntity subject) {
|
||||||
+9
-9
@@ -1,23 +1,23 @@
|
|||||||
package net.hostsharing.hsadminng.hs.accounts;
|
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.EntityPatcher;
|
||||||
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
import net.hostsharing.hsadminng.mapper.OptionalFromJson;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> {
|
public class HsProfileEntityPatcher implements EntityPatcher<ProfilePatchResource> {
|
||||||
|
|
||||||
private CredentialContextResourceToEntityMapper contextMapper;
|
private ScopeResourceToEntityMapper scopeMapper;
|
||||||
private final HsCredentialsEntity entity;
|
private final HsProfileEntity entity;
|
||||||
|
|
||||||
public HsCredentialsEntityPatcher(final CredentialContextResourceToEntityMapper contextMapper, final HsCredentialsEntity entity) {
|
public HsProfileEntityPatcher(final ScopeResourceToEntityMapper scopeMapper, final HsProfileEntity entity) {
|
||||||
this.contextMapper = contextMapper;
|
this.scopeMapper = scopeMapper;
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(final CredentialsPatchResource resource) {
|
public void apply(final ProfilePatchResource resource) {
|
||||||
if ( resource.getActive() != null ) {
|
if ( resource.getActive() != null ) {
|
||||||
entity.setActive(resource.getActive());
|
entity.setActive(resource.getActive());
|
||||||
}
|
}
|
||||||
@@ -29,8 +29,8 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc
|
|||||||
.ifPresent(entity::setSmsNumber);
|
.ifPresent(entity::setSmsNumber);
|
||||||
OptionalFromJson.of(resource.getPhonePassword())
|
OptionalFromJson.of(resource.getPhonePassword())
|
||||||
.ifPresent(entity::setPhonePassword);
|
.ifPresent(entity::setPhonePassword);
|
||||||
if (resource.getContexts() != null) {
|
if (resource.getScopes() != null) {
|
||||||
contextMapper.syncCredentialsContextEntities(resource.getContexts(), entity.getLoginContexts());
|
scopeMapper.syncProfileScopeEntities(resource.getScopes(), entity.getScopes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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 HsProfileRepository extends Repository<HsProfileEntity, UUID> {
|
||||||
|
|
||||||
|
@Timed("app.login.profile.repo.findByUuid")
|
||||||
|
Optional<HsProfileEntity> findByUuid(final UUID uuid);
|
||||||
|
|
||||||
|
@Timed("app.login.profile.repo.findByPerson")
|
||||||
|
List<HsProfileEntity> 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<HsProfileEntity> findByCurrentSubject();
|
||||||
|
|
||||||
|
@Timed("app.login.profile.repo.save")
|
||||||
|
HsProfileEntity save(final HsProfileEntity entity);
|
||||||
|
}
|
||||||
+9
-7
@@ -27,14 +27,16 @@ import static net.hostsharing.hsadminng.repr.Symbol.symbol;
|
|||||||
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
@AllArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
@SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true)
|
@SuperBuilder(builderMethodName = "baseBuilder", toBuilder = true)
|
||||||
@MappedSuperclass
|
@MappedSuperclass
|
||||||
public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<HsCredentialsContext> {
|
public abstract class HsProfileScope implements Stringifyable, BaseEntity<HsProfileScope> {
|
||||||
|
|
||||||
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
|
private static Stringify<HsProfileScope> stringify = stringify(HsProfileScope.class, "scope")
|
||||||
.withProp(HsCredentialsContext::getType)
|
.withProp(HsProfileScope::getType)
|
||||||
.withProp(HsCredentialsContext::getQualifier)
|
.withProp(HsProfileScope::getQualifier)
|
||||||
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
|
.withProp(
|
||||||
|
HsProfileScope::isOnlyForNaturalPersons,
|
||||||
value -> value ? symbol("NP-ONLY") : null)
|
value -> value ? symbol("NP-ONLY") : null)
|
||||||
.withProp(HsCredentialsContext::isPublicAccess,
|
.withProp(
|
||||||
|
HsProfileScope::isPublicAccess,
|
||||||
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
|
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
|
||||||
.quotedValues(false)
|
.quotedValues(false)
|
||||||
.withSeparator(":");
|
.withSeparator(":");
|
||||||
@@ -61,7 +63,7 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
|
|||||||
@Column(name = "public_access")
|
@Column(name = "public_access")
|
||||||
private boolean publicAccess;
|
private boolean publicAccess;
|
||||||
|
|
||||||
public boolean isHsadminContext() {
|
public boolean isHsadminScope() {
|
||||||
return "HSADMIN".equals(type);
|
return "HSADMIN".equals(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
+9
-9
@@ -6,8 +6,8 @@ import io.micrometer.core.annotation.Timed;
|
|||||||
import lombok.val;
|
import lombok.val;
|
||||||
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
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.api.ScopesApi;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@NoSecurityRequirement
|
@NoSecurityRequirement
|
||||||
public class HsCredentialsContextsController implements ContextsApi {
|
public class HsProfileScopeController implements ScopesApi {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
@@ -26,20 +26,20 @@ public class HsCredentialsContextsController implements ContextsApi {
|
|||||||
private StrictMapper mapper;
|
private StrictMapper mapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsContextRbacRepository contextRepo;
|
private HsProfileScopeRbacRepository scopeRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.contexts.getListOfLoginContexts")
|
@Timed("app.accounts.scopes.getListOfScopes")
|
||||||
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
|
public ResponseEntity<List<ScopeResource>> getListOfScopes(final String assumedRoles) {
|
||||||
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
|
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
val isGlobalAdmin = context.isGlobalAdmin();
|
val isGlobalAdmin = context.isGlobalAdmin();
|
||||||
final var loginContexts = contextRepo.findAll().stream().filter(
|
final var scopes = scopeRepo.findAll().stream().filter(
|
||||||
context -> context.isPublicAccess() || isGlobalAdmin
|
scope -> scope.isPublicAccess() || isGlobalAdmin
|
||||||
).toList();
|
).toList();
|
||||||
final var result = mapper.mapList(loginContexts, ContextResource.class);
|
final var result = mapper.mapList(scopes, ScopeResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -24,7 +24,7 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.WITHOUT_IMPLICIT
|
|||||||
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
||||||
|
|
||||||
@Entity
|
@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)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -32,11 +32,11 @@ import static net.hostsharing.hsadminng.rbac.generator.RbacSpec.rbacViewFor;
|
|||||||
@AttributeOverrides({
|
@AttributeOverrides({
|
||||||
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
|
@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)
|
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
|
||||||
public static RbacSpec rbacX() {
|
public static RbacSpec rbacX() {
|
||||||
return rbacViewFor("credentialsContext", HsCredentialsContextRbacEntity.class)
|
return rbacViewFor("profileScope", HsProfileScopeRbacEntity.class)
|
||||||
.withIdentityView(SQL.projection("type || ':' || qualifier"))
|
.withIdentityView(SQL.projection("type || ':' || qualifier"))
|
||||||
.withRestrictedViewOrderBy(SQL.expression("type || ':' || qualifier"))
|
.withRestrictedViewOrderBy(SQL.expression("type || ':' || qualifier"))
|
||||||
.withoutUpdatableColumns()
|
.withoutUpdatableColumns()
|
||||||
@@ -50,6 +50,6 @@ public class HsCredentialsContextRbacEntity extends HsCredentialsContext {
|
|||||||
|
|
||||||
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
|
// TODO_impl: RBAC rules for _rv do not yet work properly (remove the X)
|
||||||
public static void mainX(String[] args) throws IOException {
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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<HsProfileScopeRbacEntity, UUID> {
|
||||||
|
|
||||||
|
@Timed("app.accounts.scope.repo.findAll.rbac")
|
||||||
|
List<HsProfileScopeRbacEntity> findAll();
|
||||||
|
|
||||||
|
@Timed("app.accounts.scope.repo.findByUuid.rbac")
|
||||||
|
Optional<HsProfileScopeRbacEntity> findByUuid(final UUID id);
|
||||||
|
|
||||||
|
@Timed("app.accounts.scope.repo.findByTypeAndQualifier.rbac")
|
||||||
|
Optional<HsProfileScopeRbacEntity> findByTypeAndQualifier(@NotNull String contextType, @NotNull String qualifier);
|
||||||
|
|
||||||
|
@Timed("app.accounts.scope.repo.save.rbac")
|
||||||
|
HsProfileScopeRbacEntity save(final HsProfileScopeRbacEntity entity);
|
||||||
|
}
|
||||||
+2
-2
@@ -11,7 +11,7 @@ import lombok.Setter;
|
|||||||
import lombok.experimental.SuperBuilder;
|
import lombok.experimental.SuperBuilder;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(schema = "hs_accounts", name = "context")
|
@Table(schema = "hs_accounts", name = "scope")
|
||||||
@SuperBuilder(toBuilder = true)
|
@SuperBuilder(toBuilder = true)
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@@ -19,5 +19,5 @@ import lombok.experimental.SuperBuilder;
|
|||||||
@AttributeOverrides({
|
@AttributeOverrides({
|
||||||
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
|
@AttributeOverride(name = "uuid", column = @Column(name = "uuid"))
|
||||||
})
|
})
|
||||||
public class HsCredentialsContextRealEntity extends HsCredentialsContext {
|
public class HsProfileScopeRealEntity extends HsProfileScope {
|
||||||
}
|
}
|
||||||
@@ -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<HsProfileScopeRealEntity, UUID> {
|
||||||
|
|
||||||
|
@Timed("app.account.scope.repo.findAll.real")
|
||||||
|
List<HsProfileScopeRealEntity> findAll();
|
||||||
|
|
||||||
|
@Timed("app.account.scope.repo.findByUuid.real")
|
||||||
|
Optional<HsProfileScopeRealEntity> findByUuid(final UUID id);
|
||||||
|
|
||||||
|
@Timed("app.account.scope.repo.findByTypeAndQualifier.real")
|
||||||
|
Optional<HsProfileScopeRealEntity> findByTypeAndQualifier(@NotNull String type, @NotNull String qualifier);
|
||||||
|
|
||||||
|
@Timed("app.account.scope.repo.save.real")
|
||||||
|
HsProfileScopeRealEntity save(final HsProfileScopeRealEntity entity);
|
||||||
|
}
|
||||||
+20
-20
@@ -3,8 +3,8 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ScopeResource;
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
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.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -14,56 +14,56 @@ import java.util.Set;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class CredentialContextResourceToEntityMapper {
|
public class ScopeResourceToEntityMapper {
|
||||||
|
|
||||||
private final EntityManager em;
|
private final EntityManager em;
|
||||||
private final MessageTranslator messageTranslator;
|
private final MessageTranslator messageTranslator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public CredentialContextResourceToEntityMapper(EntityManager em, MessageTranslator messageTranslator) {
|
public ScopeResourceToEntityMapper(final EntityManager em, final MessageTranslator messageTranslator) {
|
||||||
this.em = em;
|
this.em = em;
|
||||||
this.messageTranslator = messageTranslator;
|
this.messageTranslator = messageTranslator;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Set<HsCredentialsContextRealEntity> mapCredentialsToContextEntities(
|
public Set<HsProfileScopeRealEntity> mapProfileToScopeEntities(
|
||||||
List<ContextResource> resources
|
final List<ScopeResource> resources
|
||||||
) {
|
) {
|
||||||
final var entities = new HashSet<HsCredentialsContextRealEntity>();
|
final var entities = new HashSet<HsProfileScopeRealEntity>();
|
||||||
syncCredentialsContextEntities(resources, entities);
|
syncProfileScopeEntities(resources, entities);
|
||||||
return entities;
|
return entities;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void syncCredentialsContextEntities(
|
public void syncProfileScopeEntities(
|
||||||
List<ContextResource> resources,
|
final List<ScopeResource> resources,
|
||||||
Set<HsCredentialsContextRealEntity> entities
|
final Set<HsProfileScopeRealEntity> entities
|
||||||
) {
|
) {
|
||||||
final var resourceUuids = resources.stream()
|
final var resourceUuids = resources.stream()
|
||||||
.map(ContextResource::getUuid)
|
.map(ScopeResource::getUuid)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
final var entityUuids = entities.stream()
|
final var entityUuids = entities.stream()
|
||||||
.map(HsCredentialsContextRealEntity::getUuid)
|
.map(HsProfileScopeRealEntity::getUuid)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
entities.removeIf(e -> !resourceUuids.contains(e.getUuid()));
|
entities.removeIf(e -> !resourceUuids.contains(e.getUuid()));
|
||||||
|
|
||||||
for (final var resource : resources) {
|
for (final var resource : resources) {
|
||||||
if (!entityUuids.contains(resource.getUuid())) {
|
if (!entityUuids.contains(resource.getUuid())) {
|
||||||
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
final var existingScopeEntity = em.find(HsProfileScopeRealEntity.class, resource.getUuid());
|
||||||
if (existingContextEntity == null) {
|
if (existingScopeEntity == null) {
|
||||||
throw new EntityNotFoundException(
|
throw new EntityNotFoundException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"general.{0}-{1}-not-found-or-not-accessible",
|
"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())) ||
|
if ((resource.getType() != null && !existingScopeEntity.getType().equals(resource.getType())) ||
|
||||||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
(resource.getQualifier() != null && !existingScopeEntity.getQualifier().equals(resource.getQualifier()))) {
|
||||||
throw new EntityNotFoundException(
|
throw new EntityNotFoundException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}",
|
"profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}",
|
||||||
existingContextEntity, resource));
|
existingScopeEntity, resource));
|
||||||
}
|
}
|
||||||
entities.add(existingContextEntity);
|
entities.add(existingScopeEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@ public abstract class ValidatableProperty<P extends ValidatableProperty<?, ?>, T
|
|||||||
private BiFunction<EntityManager, PropertiesProvider, T> computedBy;
|
private BiFunction<EntityManager, PropertiesProvider, T> computedBy;
|
||||||
|
|
||||||
@Accessors(makeFinal = true, chain = true, fluent = false)
|
@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)
|
@Accessors(makeFinal = true, chain = true, fluent = false)
|
||||||
private boolean readOnly;
|
private boolean readOnly;
|
||||||
|
|||||||
@@ -13,5 +13,5 @@ map:
|
|||||||
- type: string:uuid => java.util.UUID
|
- type: string:uuid => java.util.UUID
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
/api/hs/accounts/credentials/{credentialsUuid}:
|
/api/hs/accounts/profiles/{profileUuid}:
|
||||||
null: org.openapitools.jackson.nullable.JsonNullable
|
null: org.openapitools.jackson.nullable.JsonNullable
|
||||||
|
|||||||
@@ -13,20 +13,17 @@ paths:
|
|||||||
/api/hs/accounts/current:
|
/api/hs/accounts/current:
|
||||||
$ref: "current.yaml"
|
$ref: "current.yaml"
|
||||||
|
|
||||||
# Contexts
|
# Scopes
|
||||||
|
|
||||||
/api/hs/accounts/contexts:
|
/api/hs/accounts/scopes:
|
||||||
$ref: "contexts.yaml"
|
$ref: "scopes.yaml"
|
||||||
|
|
||||||
|
|
||||||
# Credentials
|
# Profile
|
||||||
|
|
||||||
/api/hs/accounts/credentials/{credentialsUuid}/used:
|
/api/hs/accounts/profiles/{profileUuid}:
|
||||||
$ref: "credentials-with-uuid-used.yaml"
|
$ref: "profile-with-uuid.yaml"
|
||||||
|
|
||||||
/api/hs/accounts/credentials/{credentialsUuid}:
|
/api/hs/accounts/profiles:
|
||||||
$ref: "credentials-with-uuid.yaml"
|
$ref: "profiles.yaml"
|
||||||
|
|
||||||
/api/hs/accounts/credentials:
|
|
||||||
$ref: "credentials.yaml"
|
|
||||||
|
|
||||||
|
|||||||
@@ -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'
|
|
||||||
@@ -2,7 +2,7 @@ get:
|
|||||||
summary: Currently logged in user data.
|
summary: Currently logged in user data.
|
||||||
description: Returns information about the currently logged in user.
|
description: Returns information about the currently logged in user.
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
operationId: getCurrentLoginUser
|
operationId: getCurrentLoginUser
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
@@ -10,7 +10,7 @@ get:
|
|||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/CurrentLoginUser'
|
$ref: 'profile-schemas.yaml#/components/schemas/CurrentLoginUser'
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
|
|||||||
+15
-22
@@ -13,7 +13,7 @@ components:
|
|||||||
globalAdmin:
|
globalAdmin:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
Credentials:
|
Profile:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
uuid:
|
uuid:
|
||||||
@@ -40,22 +40,17 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
globalGid:
|
globalGid:
|
||||||
type: number
|
type: number
|
||||||
onboardingToken:
|
scopes:
|
||||||
type: string
|
|
||||||
contexts:
|
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
|
||||||
lastUsed:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
required:
|
required:
|
||||||
- uuid
|
- uuid
|
||||||
- active
|
- active
|
||||||
- contexts
|
- scopes
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
CredentialsPatch:
|
ProfilePatch:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
totpSecrets:
|
totpSecrets:
|
||||||
@@ -73,13 +68,13 @@ components:
|
|||||||
nullable: true
|
nullable: true
|
||||||
active:
|
active:
|
||||||
type: boolean
|
type: boolean
|
||||||
contexts:
|
scopes:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
CredentialsInsert:
|
ProfileInsert:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
person.uuid:
|
person.uuid:
|
||||||
@@ -88,12 +83,6 @@ components:
|
|||||||
nickname:
|
nickname:
|
||||||
type: string
|
type: string
|
||||||
pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname
|
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:
|
emailAddress:
|
||||||
type: string
|
type: string
|
||||||
smsNumber:
|
smsNumber:
|
||||||
@@ -104,12 +93,16 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
globalGid:
|
globalGid:
|
||||||
type: number
|
type: number
|
||||||
onboardingToken:
|
phonePassword:
|
||||||
type: string
|
type: string
|
||||||
contexts:
|
totpSecrets:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
type: string
|
||||||
|
scopes:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
|
||||||
required:
|
required:
|
||||||
- person.uuid
|
- person.uuid
|
||||||
- nickname
|
- nickname
|
||||||
+17
-17
@@ -1,23 +1,23 @@
|
|||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
description: 'Fetch a single credentials its uuid, if visible for the current subject.'
|
description: 'Fetch a single profile its uuid, if visible for the current subject.'
|
||||||
operationId: getSingleCredentialsByUuid
|
operationId: getSingleProfileByUuid
|
||||||
parameters:
|
parameters:
|
||||||
- name: credentialsUuid
|
- name: profileUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: UUID of the credentials to fetch.
|
description: UUID of the profile to fetch.
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
|
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
|
||||||
|
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
@@ -26,11 +26,11 @@ get:
|
|||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.'
|
description: 'Updates a single profile identified by its uuid, if permitted for the current subject.'
|
||||||
operationId: patchCredentials
|
operationId: patchProfile
|
||||||
parameters:
|
parameters:
|
||||||
- name: credentialsUuid
|
- name: profileUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
@@ -40,14 +40,14 @@ patch:
|
|||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/CredentialsPatch'
|
$ref: 'profile-schemas.yaml#/components/schemas/ProfilePatch'
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
|
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
@@ -55,17 +55,17 @@ patch:
|
|||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.'
|
description: 'Delete a single profile identified by its uuid, if permitted for the current subject.'
|
||||||
operationId: deleteCredentialsByUuid
|
operationId: deleteProfileByUuid
|
||||||
parameters:
|
parameters:
|
||||||
- name: credentialsUuid
|
- name: profileUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
description: UUID of the credentials to delete.
|
description: UUID of the profile to delete.
|
||||||
responses:
|
responses:
|
||||||
"204":
|
"204":
|
||||||
description: No Content
|
description: No Content
|
||||||
+11
-11
@@ -1,9 +1,9 @@
|
|||||||
get:
|
get:
|
||||||
summary: Returns a list of all credentials.
|
summary: Returns a list of all profile.
|
||||||
description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles.
|
description: Returns the list of all profile which are visible to the current subject or any of it's assumed roles.
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
operationId: getListOfCredentials
|
operationId: getListOfProfile
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: personUuid
|
- name: personUuid
|
||||||
@@ -12,7 +12,7 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
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:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
@@ -21,31 +21,31 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
|
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
summary: Adds a new credentials.
|
summary: Adds a new profile.
|
||||||
tags:
|
tags:
|
||||||
- credentials
|
- profile
|
||||||
operationId: postNewCredentials
|
operationId: postNewProfile
|
||||||
requestBody:
|
requestBody:
|
||||||
description: A JSON object describing the new credential.
|
description: A JSON object describing the new credential.
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/CredentialsInsert'
|
$ref: 'profile-schemas.yaml#/components/schemas/ProfileInsert'
|
||||||
responses:
|
responses:
|
||||||
"201":
|
"201":
|
||||||
description: Created
|
description: Created
|
||||||
content:
|
content:
|
||||||
'application/json':
|
'application/json':
|
||||||
schema:
|
schema:
|
||||||
$ref: 'credentials-schemas.yaml#/components/schemas/Credentials'
|
$ref: 'profile-schemas.yaml#/components/schemas/Profile'
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
+1
-1
@@ -3,7 +3,7 @@ components:
|
|||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
|
|
||||||
Context:
|
Scope:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
uuid:
|
uuid:
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
get:
|
get:
|
||||||
summary: Returns a list of all accessible contexts.
|
summary: Returns a list of all accessible scopes.
|
||||||
description: Returns the list of all contexts which are visible to the current subject or any of it's assumed roles.
|
description: Returns the list of all scopes which are visible to the current subject or any of it's assumed roles.
|
||||||
tags:
|
tags:
|
||||||
- contexts
|
- scopes
|
||||||
operationId: getListOfContexts
|
operationId: getListOfScopes
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
responses:
|
responses:
|
||||||
@@ -14,7 +14,7 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
$ref: 'scope-schemas.yaml#/components/schemas/Scope'
|
||||||
"401":
|
"401":
|
||||||
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
"403":
|
"403":
|
||||||
@@ -75,7 +75,7 @@ metrics:
|
|||||||
# HOWTO set logging-levels for certain Java packages (trace, debug, info, warn, error)
|
# HOWTO set logging-levels for certain Java packages (trace, debug, info, warn, error)
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
# org.springframework.security: info
|
org.springframework.security: info
|
||||||
# org.hibernate.SQL: DEBUG # Enable SQL query logging
|
# org.hibernate.SQL: DEBUG # Enable SQL query logging
|
||||||
# org.hibernate.orm.jdbc.bind: TRACE # Enable SQL parameter binding logging
|
# org.hibernate.orm.jdbc.bind: TRACE # Enable SQL parameter binding logging
|
||||||
# org.springframework.web: DEBUG
|
# org.springframework.web: DEBUG
|
||||||
|
|||||||
@@ -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
|
||||||
create table rbac.subject
|
(
|
||||||
(
|
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
|
||||||
uuid uuid primary key references rbac.reference (uuid) on delete cascade,
|
name varchar(63) not null unique
|
||||||
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)
|
create or replace function rbac.create_subject(subjectName varchar)
|
||||||
returns uuid
|
returns uuid
|
||||||
returns null on null input
|
returns null on null input
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
objectId uuid;
|
stableUuidNamespace uuid;
|
||||||
|
subjectUuid uuid;
|
||||||
begin
|
begin
|
||||||
|
stableUuidNamespace := '6ba7b810-9dad-11d1-80b4-00c04fd430c8'::uuid;
|
||||||
|
subjectUuid := uuid_generate_v5(stableUuidNamespace, subjectName);
|
||||||
insert
|
insert
|
||||||
into rbac.reference (type)
|
into rbac.reference (uuid, type)
|
||||||
values ('rbac.subject')
|
values (subjectUuid, 'rbac.subject');
|
||||||
returning uuid into objectId;
|
|
||||||
insert
|
insert
|
||||||
into rbac.subject (uuid, name)
|
into rbac.subject (uuid, name)
|
||||||
values (objectid, subjectName);
|
values (subjectUuid, subjectName);
|
||||||
return objectId;
|
return subjectUuid;
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|||||||
+5
-5
@@ -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
|
-- 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 $$
|
do language plpgsql $$
|
||||||
begin
|
begin
|
||||||
call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex');
|
call hs_office.person_create_test_data('NP', null,'Hostmaster', 'Alex', true);
|
||||||
call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran');
|
call hs_office.person_create_test_data('NP', null, 'Hostmaster', 'Fran', true);
|
||||||
call hs_office.person_create_test_data('NP', null, 'User', 'Drew');
|
call hs_office.person_create_test_data('NP', null, 'User', 'Drew', true);
|
||||||
call hs_office.person_create_test_data('NP', null, 'User', 'Test');
|
call hs_office.person_create_test_data('NP', null, 'User', 'Test', true);
|
||||||
end;
|
end;
|
||||||
$$;
|
$$;
|
||||||
--//
|
--//
|
||||||
|
|||||||
+7
-3
@@ -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,
|
newPersonType hs_office.PersonType,
|
||||||
newTradeName varchar,
|
newTradeName varchar,
|
||||||
newFamilyName varchar = null,
|
newFamilyName varchar = null,
|
||||||
newGivenName varchar = null
|
newGivenName varchar = null,
|
||||||
|
ignoreIfExists boolean = false
|
||||||
)
|
)
|
||||||
language plpgsql as $$
|
language plpgsql as $$
|
||||||
declare
|
declare
|
||||||
@@ -22,7 +23,10 @@ begin
|
|||||||
fullName := concat_ws(', ', newTradeName, newFamilyName, newGivenName);
|
fullName := concat_ws(', ', newTradeName, newFamilyName, newGivenName);
|
||||||
emailAddr = 'person-' || left(base.cleanIdentifier(fullName), 32) || '@example.com';
|
emailAddr = 'person-' || left(base.cleanIdentifier(fullName), 32) || '@example.com';
|
||||||
call base.defineContext('creating person test-data');
|
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);
|
call base.defineContext('creating person test-data', null, emailAddr);
|
||||||
|
|
||||||
raise notice 'creating test person: % by %', fullName, emailAddr;
|
raise notice 'creating test person: % by %', fullName, emailAddr;
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:hs-credentials-SCHEMA endDelimiter:--//
|
--changeset michael.hoennig:hs-profile-SCHEMA endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
CREATE SCHEMA hs_accounts;
|
CREATE SCHEMA hs_accounts;
|
||||||
--//
|
--//
|
||||||
|
|||||||
@@ -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,
|
uuid uuid PRIMARY KEY references rbac.subject (uuid) initially deferred,
|
||||||
version int not null default 0,
|
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),
|
person_uuid uuid not null references hs_office.person(uuid),
|
||||||
|
|
||||||
active bool,
|
active bool,
|
||||||
last_used timestamp,
|
|
||||||
global_uid int unique, -- w/o
|
global_uid int unique, -- w/o
|
||||||
global_gid 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[],
|
totp_secrets text[],
|
||||||
phone_password 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,
|
uuid uuid PRIMARY KEY,
|
||||||
version int not null default 0,
|
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 $$
|
RETURNS TRIGGER AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
RAISE EXCEPTION 'Updates to hs_accounts.context are not allowed.';
|
RAISE EXCEPTION 'Updates to hs_accounts.scope are not allowed.';
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
-- Trigger to enforce immutability
|
-- Trigger to enforce immutability
|
||||||
CREATE TRIGGER context_immutable_trigger
|
CREATE TRIGGER scope_immutable_trigger
|
||||||
BEFORE UPDATE ON hs_accounts.context
|
BEFORE UPDATE ON hs_accounts.scope
|
||||||
FOR EACH ROW EXECUTE FUNCTION hs_accounts.prevent_context_update();
|
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(),
|
uuid uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
credentials_uuid uuid references hs_accounts.credentials(uuid) ON DELETE CASCADE,
|
profile_uuid uuid references hs_accounts.profile(uuid) ON DELETE CASCADE,
|
||||||
context_uuid uuid references hs_accounts.context(uuid) ON DELETE RESTRICT
|
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:--//
|
--changeset michael.hoennig:hs-hs_accounts-JOURNALS endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
|
|
||||||
call base.create_journal('hs_accounts.context_mapping');
|
call base.create_journal('hs_accounts.scope_mapping');
|
||||||
call base.create_journal('hs_accounts.context');
|
call base.create_journal('hs_accounts.scope');
|
||||||
call base.create_journal('hs_accounts.credentials');
|
call base.create_journal('hs_accounts.profile');
|
||||||
--//
|
--//
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================================
|
-- ============================================================================
|
||||||
--changeset michael.hoennig:hs_accounts-HISTORICIZATION endDelimiter:--//
|
--changeset michael.hoennig:hs_accounts-HISTORICIZATION endDelimiter:--//
|
||||||
-- ----------------------------------------------------------------------------
|
-- ----------------------------------------------------------------------------
|
||||||
call base.tx_create_historicization('hs_accounts.context_mapping');
|
call base.tx_create_historicization('hs_accounts.scope_mapping');
|
||||||
call base.tx_create_historicization('hs_accounts.context');
|
call base.tx_create_historicization('hs_accounts.scope');
|
||||||
call base.tx_create_historicization('hs_accounts.credentials');
|
call base.tx_create_historicization('hs_accounts.profile');
|
||||||
--//
|
--//
|
||||||
|
|||||||
-41
@@ -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
|
|
||||||
|
|
||||||
```
|
|
||||||
+41
@@ -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
|
||||||
|
|
||||||
|
```
|
||||||
+37
-37
@@ -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 $$
|
do language plpgsql $$
|
||||||
@@ -16,11 +16,11 @@ declare
|
|||||||
personDrewUuid uuid;
|
personDrewUuid uuid;
|
||||||
|
|
||||||
|
|
||||||
context_HSADMIN_prod hs_accounts.context;
|
scope_HSADMIN_prod hs_accounts.scope;
|
||||||
context_SSH_internal hs_accounts.context;
|
scope_SSH_internal hs_accounts.scope;
|
||||||
context_SSH_external hs_accounts.context;
|
scope_SSH_external hs_accounts.scope;
|
||||||
context_MATRIX_internal hs_accounts.context;
|
scope_MATRIX_internal hs_accounts.scope;
|
||||||
context_MATRIX_external hs_accounts.context;
|
scope_MATRIX_external hs_accounts.scope;
|
||||||
|
|
||||||
begin
|
begin
|
||||||
call base.defineContext('creating booking-project test-data', null, 'superuser-alex@hostsharing.net', 'rbac.global#global:ADMIN');
|
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');
|
userDrewSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='selfregistered-user-drew@hostsharing.org');
|
||||||
personDrewUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Drew');
|
personDrewUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Drew');
|
||||||
|
|
||||||
-- Add test contexts
|
-- Add test scopes
|
||||||
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
|
||||||
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true)
|
('11111111-1111-1111-1111-111111111111', 'HSADMIN', 'prod', true, true)
|
||||||
RETURNING * INTO context_HSADMIN_prod;
|
RETURNING * INTO scope_HSADMIN_prod;
|
||||||
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
|
||||||
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false)
|
('22222222-2222-2222-2222-222222222222', 'SSH', 'internal', true, false)
|
||||||
RETURNING * INTO context_SSH_internal;
|
RETURNING * INTO scope_SSH_internal;
|
||||||
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
|
||||||
('33333333-3333-3333-3333-333333333333', 'SSH', 'external', false, true)
|
('33333333-3333-3333-3333-333333333333', 'SSH', 'external', false, true)
|
||||||
RETURNING * INTO context_SSH_external;
|
RETURNING * INTO scope_SSH_external;
|
||||||
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
|
||||||
('44444444-4444-4444-4444-444444444444', 'MATRIX', 'internal', true, false)
|
('44444444-4444-4444-4444-444444444444', 'MATRIX', 'internal', true, false)
|
||||||
RETURNING * INTO context_MATRIX_internal;
|
RETURNING * INTO scope_MATRIX_internal;
|
||||||
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
|
||||||
('55555555-5555-5555-5555-555555555555', 'MATRIX', 'external', true, true)
|
('55555555-5555-5555-5555-555555555555', 'MATRIX', 'external', true, true)
|
||||||
RETURNING * INTO context_MATRIX_external;
|
RETURNING * INTO scope_MATRIX_external;
|
||||||
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
|
||||||
('66666666-6666-6666-6666-666666666666', 'MASTODON', 'external', false, true);
|
('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);
|
('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
|
-- TODO_impl: RBAC rules for _rv do not yet work properly
|
||||||
-- call rbac.grantPermissiontoRole(
|
-- call rbac.grantPermissiontoRole(
|
||||||
-- rbac.createPermission(context_HSADMIN_prod.uuid, 'SELECT'),
|
-- rbac.createPermission(context_HSADMIN_prod.uuid, 'SELECT'),
|
||||||
@@ -64,25 +64,25 @@ begin
|
|||||||
-- call rbac.grantPermissionToRole(
|
-- call rbac.grantPermissionToRole(
|
||||||
-- rbac.createPermission(context_MATRIX_internal.uuid, 'SELECT'),
|
-- rbac.createPermission(context_MATRIX_internal.uuid, 'SELECT'),
|
||||||
-- rbac.global_ADMIN());
|
-- rbac.global_ADMIN());
|
||||||
-- call rbac.grantRoleToRole(hs_accounts.context_REFERRER(context_SSH_internal), rbac.global_ADMIN());
|
-- call rbac.grantRoleToRole(hs_accounts.scope_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_MATRIX_internal), rbac.global_ADMIN());
|
||||||
|
|
||||||
-- Add test credentials (linking to assumed rbac.subject UUIDs)
|
-- Add test profile (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
|
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, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'),
|
( 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, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'),
|
( 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, 'token-def', ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777');
|
( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777');
|
||||||
|
|
||||||
-- Map credentials to contexts
|
-- Map profile to contexts
|
||||||
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
|
INSERT INTO hs_accounts.scope_mapping (profile_uuid, scope_uuid) VALUES
|
||||||
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
|
(superuserAlexSubjectUuid, scope_HSADMIN_prod.uuid),
|
||||||
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
|
(superuserFranSubjectUuid, scope_HSADMIN_prod.uuid),
|
||||||
(userDrewSubjectUuid, context_HSADMIN_prod.uuid),
|
(userDrewSubjectUuid, scope_HSADMIN_prod.uuid),
|
||||||
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
|
(superuserAlexSubjectUuid, scope_SSH_internal.uuid),
|
||||||
(superuserFranSubjectUuid, context_SSH_internal.uuid),
|
(superuserFranSubjectUuid, scope_SSH_internal.uuid),
|
||||||
(userDrewSubjectUuid, context_SSH_external.uuid),
|
(userDrewSubjectUuid, scope_SSH_external.uuid),
|
||||||
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
|
(superuserAlexSubjectUuid, scope_MATRIX_internal.uuid),
|
||||||
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
|
(superuserFranSubjectUuid, scope_MATRIX_internal.uuid);
|
||||||
|
|
||||||
end; $$;
|
end; $$;
|
||||||
--//
|
--//
|
||||||
|
|||||||
@@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - auf Deutsch
|
|||||||
test.ponged-{0}--in-your-language=ponged {0} - auf Deutsch
|
test.ponged-{0}--in-your-language=ponged {0} - auf Deutsch
|
||||||
test.available-in-all-properties-files=Hallo {0} - DE!
|
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 validations
|
||||||
general.{0}-{1}-not-found={0} "{1}" nicht gefunden
|
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.{0}-{1}-not-found-or-not-accessible={0} "{1}" nicht gefunden oder nicht zugänglich
|
||||||
general.but-is=ist aber
|
general.but-is=ist aber
|
||||||
|
|
||||||
# credentials validations
|
# profile validations
|
||||||
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
profile.existing-profile-scope-{0}-does-not-match-given-resource-{1}=existierender Gültigkeitsbereich {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
|
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
|
||||||
credentials.access-denied-for-contexts-{0}=Kontext-Zugriff verweigert: {0}
|
profile.access-denied-for-scopes-{0}=Zugriff auf Geltungsbereich verweigert: {0}
|
||||||
credentials.context-requires-natural-person-{0}=Kontext verlangt eine natürliche Person: {0}
|
profile.scope-requires-natural-person-{0}=Geltungsbereich verlangt eine natürliche Person: {0}
|
||||||
credentials.own-hsadmin-credentials-must-not-be-removed=die eigenen hsadmin-Credentials dürfen nicht entfernt werden
|
profile.own-hsadmin-profile-must-not-be-removed=die eigenen hsadmin-Profile dürfen nicht entfernt werden
|
||||||
|
|
||||||
# office.coop-shares
|
# 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}
|
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
|
||||||
|
|||||||
@@ -5,21 +5,17 @@ test.pinged--in-your-language=pinged - in English
|
|||||||
test.ponged-{0}--in-your-language=ponged {0} - in English
|
test.ponged-{0}--in-your-language=ponged {0} - in English
|
||||||
test.available-in-all-properties-files=Hello {0} - EN!
|
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 validations
|
||||||
general.{0}-{1}-not-found={0} "{1}" not found
|
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.{0}-{1}-not-found-or-not-accessible={0} "{1}" not found or not accessible
|
||||||
general.but-is=but is
|
general.but-is=but is
|
||||||
|
|
||||||
# credentials validations
|
# profile validations
|
||||||
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
|
profile.existing-profile-scope-{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
|
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
|
||||||
credentials.access-denied-for-contexts-{0}=context-access denied: {0}
|
profile.access-denied-for-scopes-{0}=scope-access denied: {0}
|
||||||
credentials.context-requires-natural-person-{0}=context requires natural person: {0}
|
profile.scope-requires-natural-person-{0}=scope requires natural person: {0}
|
||||||
credentials.own-hsadmin-credentials-must-not-be-removed=own hsadmin-credentials must not be removed
|
profile.own-hsadmin-profile-must-not-be-removed=own hsadmin-profile must not be removed
|
||||||
|
|
||||||
# office.coop-shares
|
# 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}
|
office.coop-shares.for-transactiontype-{0}-sharecount-must-be-positive-but-is-{1}=for transactiontType {0} shareCount must be positive but is {1}
|
||||||
|
|||||||
@@ -5,21 +5,17 @@
|
|||||||
test.pinged--in-your-language=ponged {0} - en Francais
|
test.pinged--in-your-language=ponged {0} - en Francais
|
||||||
test.available-in-all-properties-files=Salut {0} - FR!
|
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 validations
|
||||||
general.{0}-{1}-not-found={0} "{1}" non trouvé
|
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.{0}-{1}-not-found-or-not-accessible={0} "{1}" non trouvé ou non accessible
|
||||||
general.but-is=mais c'est
|
general.but-is=mais c'est
|
||||||
|
|
||||||
# credentials validations
|
# profile validations
|
||||||
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
|
profile.existing-profile-scope-{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
|
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
|
||||||
credentials.access-denied-for-contexts-{0}=accès au contexte refusé : {0}
|
profile.access-denied-for-scopes-{0}=accès au domaine d'application refusé : {0}
|
||||||
credentials.context-requires-natural-person-{0}=le contexte requiert une personne physique : {0}
|
profile.scope-requires-natural-person-{0}=le domaine d'application requiert une personne physique : {0}
|
||||||
credentials.own-hsadmin-credentials-must-not-be-removed=suppression des identifiants hsadmin propres interdite
|
profile.own-hsadmin-profile-must-not-be-removed=suppression des identifiants hsadmin propres interdite
|
||||||
|
|
||||||
# office.coop-shares
|
# 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}
|
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}
|
||||||
|
|||||||
-168
@@ -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<String> 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<String> 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<HsCredentialsContextRealEntity> initialContextEntities = Set.of(initialContextEntity1, initialContextEntity2);
|
|
||||||
private List<ContextResource> patchedContextResources;
|
|
||||||
private final Set<HsCredentialsContextRealEntity> 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<Property> 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()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+78
-115
@@ -4,7 +4,7 @@ import io.restassured.RestAssured;
|
|||||||
import io.restassured.http.ContentType;
|
import io.restassured.http.ContentType;
|
||||||
import lombok.val;
|
import lombok.val;
|
||||||
import net.hostsharing.hsadminng.rbac.context.Context;
|
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.HsOfficePersonRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
@@ -27,7 +27,6 @@ import jakarta.persistence.PersistenceContext;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
import static net.hostsharing.hsadminng.config.JwtFakeBearer.bearer;
|
||||||
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON;
|
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.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.containsString;
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
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")
|
@Tag("generalIntegrationTest")
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -47,7 +43,7 @@ import static org.hamcrest.Matchers.nullValue;
|
|||||||
)
|
)
|
||||||
@ActiveProfiles("fake-jwt")
|
@ActiveProfiles("fake-jwt")
|
||||||
// too complex database interaction for just a RestTest, thus a fully integrated test
|
// too complex database interaction for just a RestTest, thus a fully integrated test
|
||||||
class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
class HsProfileControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
Integer port;
|
Integer port;
|
||||||
@@ -62,13 +58,13 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
HsOfficePersonRealRepository realPersonRepo;
|
HsOfficePersonRealRepository realPersonRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
HsCredentialsContextRealRepository contextRepo;
|
HsProfileScopeRealRepository scopeRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
HsCredentialsRepository credentialsRepo;
|
HsProfileRepository profileRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
HsProfileScopeRbacRepository scopeRbacRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
JpaAttempt jpaAttempt;
|
JpaAttempt jpaAttempt;
|
||||||
@@ -105,23 +101,23 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class GetCredentialsByUuid {
|
class GetProfileByUuid {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFilterInvalidContextsRegardingNonNaturalPerson() {
|
void shouldFilterInvalidScopesRegardingNonNaturalPerson() {
|
||||||
// given
|
// given
|
||||||
val legalPerson = givenLegalPerson("selfregistered-user-drew@hostsharing.org");
|
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 -> {
|
"test-subject1", legalPerson, builder -> {
|
||||||
builder.loginContexts(new HashSet<>(contextRepo.findAll()));
|
builder.scopes(new HashSet<>(scopeRepo.findAll()));
|
||||||
});
|
});
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
.header("Authorization", bearer(credentialsEntity.getSubject().getName()))
|
.header("Authorization", bearer(profileEntity.getSubject().getName()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.get("http://localhost/api/hs/accounts/credentials/" + credentialsEntity.getUuid())
|
.get("http://localhost/api/hs/accounts/profiles/" + profileEntity.getUuid())
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(200)
|
.statusCode(200)
|
||||||
.contentType("application/json")
|
.contentType("application/json")
|
||||||
@@ -143,8 +139,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"active": false,
|
"active": false,
|
||||||
"globalUid": null,
|
"globalUid": null,
|
||||||
"globalGid": null,
|
"globalGid": null,
|
||||||
"onboardingToken": null,
|
"scopes": [
|
||||||
"contexts": [
|
|
||||||
{
|
{
|
||||||
"uuid": "33333333-3333-3333-3333-333333333333",
|
"uuid": "33333333-3333-3333-3333-333333333333",
|
||||||
"type": "SSH",
|
"type": "SSH",
|
||||||
@@ -166,8 +161,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"onlyForNaturalPersons": false,
|
"onlyForNaturalPersons": false,
|
||||||
"publicAccess": true
|
"publicAccess": true
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"lastUsed": null
|
|
||||||
}
|
}
|
||||||
"""));
|
"""));
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
@@ -175,14 +169,14 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class PostNewCredentials {
|
class PostNewProfile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectCreatingCredentialsForUnrepresentedPerson() {
|
void shouldRejectCreatingProfileForUnrepresentedPerson() {
|
||||||
// given
|
// given
|
||||||
val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org");
|
val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org");
|
||||||
val publicContext = contextRepo.findByTypeAndQualifier("SSH", "external").orElseThrow();
|
val publicScope = scopeRepo.findByTypeAndQualifier("SSH", "external").orElseThrow();
|
||||||
assertThat(publicContext.isPublicAccess()).as("precondition failed").isTrue();
|
assertThat(publicScope.isPublicAccess()).as("precondition failed").isTrue();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@@ -196,16 +190,16 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"active": true,
|
"active": true,
|
||||||
"globalUid": 30001,
|
"globalUid": 30001,
|
||||||
"globalGid": 40001,
|
"globalGid": 40001,
|
||||||
"contexts": [
|
"scopes": [
|
||||||
{
|
{
|
||||||
"uuid" : "%s"
|
"uuid" : "%s"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""".formatted(testPerson.getUuid(), publicContext.getUuid()))
|
""".formatted(testPerson.getUuid(), publicScope.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.post("http://localhost/api/hs/accounts/credentials")
|
.post("http://localhost/api/hs/accounts/profiles")
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(400)
|
.statusCode(400)
|
||||||
.contentType("application/json")
|
.contentType("application/json")
|
||||||
@@ -214,15 +208,15 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectCreatingCredentialsWithPrivateContextForNormalUser() {
|
void shouldRejectCreatingProfileWithPrivateScopeForNormalUser() {
|
||||||
// given
|
// given
|
||||||
val drewPerson = realPersonRepo.findPersonByOptionalNameLike("Drew").getFirst();
|
val drewPerson = realPersonRepo.findPersonByOptionalNameLike("Drew").getFirst();
|
||||||
val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal")
|
val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||||
val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal")
|
val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||||
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@@ -236,7 +230,7 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"active": true,
|
"active": true,
|
||||||
"globalUid": 30001,
|
"globalUid": 30001,
|
||||||
"globalGid": 40001,
|
"globalGid": 40001,
|
||||||
"contexts": [
|
"scopes": [
|
||||||
{ "uuid" : "%s" },
|
{ "uuid" : "%s" },
|
||||||
{ "uuid" : "%s" },
|
{ "uuid" : "%s" },
|
||||||
{ "uuid" : "%s" }
|
{ "uuid" : "%s" }
|
||||||
@@ -244,25 +238,25 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
drewPerson.getUuid(),
|
drewPerson.getUuid(),
|
||||||
publicExternalMatrixContext.getUuid(),
|
publicExternalMatrixScope.getUuid(),
|
||||||
privateInternalSshContext.getUuid(),
|
privateInternalSshScope.getUuid(),
|
||||||
privateInternalMatrixContext.getUuid()))
|
privateInternalMatrixScope.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.post("http://localhost/api/hs/accounts/credentials")
|
.post("http://localhost/api/hs/accounts/profiles")
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(400)
|
.statusCode(400)
|
||||||
.contentType("application/json")
|
.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
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectCreatingCredentialsWithNaturalPersonRequirementForNonNaturalPerson() {
|
void shouldRejectCreatingProfileWithNaturalPersonRequirementForNonNaturalPerson() {
|
||||||
// given
|
// given
|
||||||
val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst();
|
val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst();
|
||||||
val hsadminProdContextOnlyForNaturalPersons = contextRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
val hsadminProdScopeOnlyForNaturalPersons = scopeRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asNaturalPersonContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asNaturalPersonScope).orElseThrow();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@@ -276,39 +270,39 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
"active": true,
|
"active": true,
|
||||||
"globalUid": 30001,
|
"globalUid": 30001,
|
||||||
"globalGid": 40001,
|
"globalGid": 40001,
|
||||||
"contexts": [
|
"scopes": [
|
||||||
{ "uuid" : "%s" }
|
{ "uuid" : "%s" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
firstGmbHPerson.getUuid(),
|
firstGmbHPerson.getUuid(),
|
||||||
hsadminProdContextOnlyForNaturalPersons.getUuid()))
|
hsadminProdScopeOnlyForNaturalPersons.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.post("http://localhost/api/hs/accounts/credentials")
|
.post("http://localhost/api/hs/accounts/profiles")
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(400)
|
.statusCode(400)
|
||||||
.contentType("application/json")
|
.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
|
// @formatter:on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class PatchCredentials {
|
class PatchProfile {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectPatchingCredentialsWithPrivateContextForNormalUser() {
|
void shouldRejectPatchingProfileWithPrivateScopeForNormalUser() {
|
||||||
// given
|
// given
|
||||||
context.define("selfregistered-user-drew@hostsharing.org");
|
context.define("selfregistered-user-drew@hostsharing.org");
|
||||||
val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||||
.getSubject().getUuid();
|
.getSubject().getUuid();
|
||||||
val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal")
|
val privateInternalSshScope = scopeRepo.findByTypeAndQualifier("SSH", "internal")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||||
val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal")
|
val privateInternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPrivateScope).orElseThrow();
|
||||||
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@@ -317,34 +311,34 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"contexts": [
|
"scopes": [
|
||||||
{ "uuid" : "%s" },
|
{ "uuid" : "%s" },
|
||||||
{ "uuid" : "%s" },
|
{ "uuid" : "%s" },
|
||||||
{ "uuid" : "%s" }
|
{ "uuid" : "%s" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""".formatted(
|
""".formatted(
|
||||||
privateInternalSshContext.getUuid(),
|
privateInternalSshScope.getUuid(),
|
||||||
publicExternalMatrixContext.getUuid(),
|
publicExternalMatrixScope.getUuid(),
|
||||||
privateInternalMatrixContext.getUuid()))
|
privateInternalMatrixScope.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid)
|
.patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid)
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(400)
|
.statusCode(400)
|
||||||
.contentType("application/json")
|
.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
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRejectPatchingCredentialsAndRemovingTheOwnHsadminCredentials() {
|
void shouldRejectPatchingProfileAndRemovingTheOwnHsadminProfile() {
|
||||||
// given
|
// given
|
||||||
context.define("selfregistered-user-drew@hostsharing.org");
|
context.define("selfregistered-user-drew@hostsharing.org");
|
||||||
val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
val drewProfileUuid = profileRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||||
.getSubject().getUuid();
|
.getSubject().getUuid();
|
||||||
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
val publicExternalMatrixScope = scopeRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
.map(HsProfileControllerAcceptanceTest::asPublicScope).orElseThrow();
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
@@ -353,49 +347,18 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.body("""
|
.body("""
|
||||||
{
|
{
|
||||||
"contexts": [
|
"scopes": [
|
||||||
{ "uuid" : "%s" }
|
{ "uuid" : "%s" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
""".formatted(publicExternalMatrixContext.getUuid()))
|
""".formatted(publicExternalMatrixScope.getUuid()))
|
||||||
.port(port)
|
.port(port)
|
||||||
.when()
|
.when()
|
||||||
.patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid)
|
.patch("http://localhost/api/hs/accounts/profiles/" + drewProfileUuid)
|
||||||
.then().log().all().assertThat()
|
.then().log().all().assertThat()
|
||||||
.statusCode(400)
|
.statusCode(400)
|
||||||
.contentType("application/json")
|
.contentType("application/json")
|
||||||
.body("message", containsString("die eigenen hsadmin-Credentials dürfen nicht entfernt werden"));
|
.body("message", containsString("die eigenen hsadmin-Profile 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())));
|
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,25 +396,25 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
}).returnedValue();
|
}).returnedValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HsCredentialsContextRealEntity asNaturalPersonContext(@NotNull HsCredentialsContextRealEntity context) {
|
private static HsProfileScopeRealEntity asNaturalPersonScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||||
assertThat(context.isOnlyForNaturalPersons()).as("precondition failed").isTrue();
|
assertThat(scope.isOnlyForNaturalPersons()).as("precondition failed").isTrue();
|
||||||
return context;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HsCredentialsContextRealEntity asPrivateContext(@NotNull HsCredentialsContextRealEntity context) {
|
private static HsProfileScopeRealEntity asPrivateScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||||
assertThat(context.isPublicAccess()).as("precondition failed").isFalse();
|
assertThat(scope.isPublicAccess()).as("precondition failed").isFalse();
|
||||||
return context;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static HsCredentialsContextRealEntity asPublicContext(@NotNull HsCredentialsContextRealEntity context) {
|
private static HsProfileScopeRealEntity asPublicScope(@NotNull HsProfileScopeRealEntity scope) {
|
||||||
assertThat(context.isPublicAccess()).as("precondition failed").isTrue();
|
assertThat(scope.isPublicAccess()).as("precondition failed").isTrue();
|
||||||
return context;
|
return scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HsCredentialsEntity givenNewCredentials(
|
private HsProfileEntity givenNewProfile(
|
||||||
final String executingSubjectName,
|
final String executingSubjectName,
|
||||||
final String newSubjectName, final HsOfficePersonRealEntity person,
|
final String newSubjectName, final HsOfficePersonRealEntity person,
|
||||||
final Consumer<HsCredentialsEntityBuilder> modifier
|
final Consumer<HsProfileEntityBuilder> modifier
|
||||||
) {
|
) {
|
||||||
return jpaAttempt.transacted(() -> {
|
return jpaAttempt.transacted(() -> {
|
||||||
context.define(executingSubjectName);
|
context.define(executingSubjectName);
|
||||||
@@ -462,12 +425,12 @@ class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
context.define(subject.getName());
|
context.define(subject.getName());
|
||||||
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
||||||
val credentialsBuilder = HsCredentialsEntity.builder()
|
val profileBuilder = HsProfileEntity.builder()
|
||||||
.person(attachedPerson)
|
.person(attachedPerson)
|
||||||
.subject(subjectRepo.findByUuid(subject.getUuid()))
|
.subject(subjectRepo.findByUuid(subject.getUuid()))
|
||||||
.loginContexts(Set.of());
|
.scopes(Set.of());
|
||||||
modifier.accept(credentialsBuilder);
|
modifier.accept(profileBuilder);
|
||||||
return toCleanup(credentialsRepo.save(credentialsBuilder.build()));
|
return toCleanup(profileRepo.save(profileBuilder.build()));
|
||||||
}).assertSuccessful().returnedValue();
|
}).assertSuccessful().returnedValue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+167
@@ -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<String> 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<String> 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<HsProfileScopeRealEntity> initialScopeEntities = Set.of(initialScopeEntity1, initialScopeEntity2);
|
||||||
|
private List<ScopeResource> patchedScopeResources;
|
||||||
|
private final Set<HsProfileScopeRealEntity> 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<Property> 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
-51
@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.catchThrowable;
|
|||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
@Import({ Context.class, JpaAttempt.class })
|
@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_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||||
private static final String SUPERUSER_FRAN_SUBJECT_NAME = "superuser-fran@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;
|
private HsOfficePersonRealRepository personRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepository;
|
private HsProfileRepository profileRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsContextRealRepository loginContextRealRepo;
|
private HsProfileScopeRealRepository scopeRealRepo;
|
||||||
|
|
||||||
// fetched UUIDs from test-data
|
// fetched UUIDs from test-data
|
||||||
private RbacSubjectEntity alexSubject;
|
private RbacSubjectEntity alexSubject;
|
||||||
@@ -76,7 +76,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
@Test
|
@Test
|
||||||
public void historizationIsAvailable() {
|
public void historizationIsAvailable() {
|
||||||
// given
|
// given
|
||||||
final String nativeQuerySql = "select * from hs_accounts.credentials_hv";
|
final String nativeQuerySql = "select * from hs_accounts.profile_hv";
|
||||||
|
|
||||||
// when
|
// when
|
||||||
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
||||||
@@ -85,7 +85,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsBefore)
|
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);
|
.hasSize(0);
|
||||||
|
|
||||||
// and when
|
// and when
|
||||||
@@ -95,86 +95,86 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsAfter)
|
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);
|
.hasSize(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void representativeShouldFindOwnAndRepresentedCredentialsByCurrentSubject() {
|
void representativeShouldFindOwnAndRepresentedProfileByCurrentSubject() {
|
||||||
// given
|
// given
|
||||||
final var firstGmbHPerson = givenPerson("First GmbH");
|
final var firstGmbHPerson = givenPerson("First GmbH");
|
||||||
givenRelation(REPRESENTATIVE)
|
givenRelation(REPRESENTATIVE)
|
||||||
.withAnchorPersonLike(firstGmbHPerson)
|
.withAnchorPersonLike(firstGmbHPerson)
|
||||||
.withHolder(drewPerson)
|
.withHolder(drewPerson)
|
||||||
.withContact("some test contact");
|
.withContact("some test contact");
|
||||||
givenCredentials()
|
givenProfile()
|
||||||
.forSubject("first-gmbh")
|
.forSubject("first-gmbh")
|
||||||
.forPerson(firstGmbHPerson)
|
.forPerson(firstGmbHPerson)
|
||||||
.withEMailAddress("first-gmbh@example.com");
|
.withEMailAddress("first-gmbh@example.com");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundCredentials = attempt(
|
final var foundProfile = attempt(
|
||||||
em, () -> {
|
em, () -> {
|
||||||
context(USER_DREW_SUBJECT_NAME);
|
context(USER_DREW_SUBJECT_NAME);
|
||||||
return credentialsRepository.findByCurrentSubject();
|
return profileRepository.findByCurrentSubject();
|
||||||
})
|
})
|
||||||
.assertNotNull().returnedValue();
|
.assertNotNull().returnedValue();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundCredentials).hasSize(2)
|
assertThat(foundProfile).hasSize(2)
|
||||||
.map(HsCredentialsEntity::getEmailAddress)
|
.map(HsProfileEntity::getEmailAddress)
|
||||||
.containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com");
|
.containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void globalAdminShouldFindOnlyOwnCredentialsByCurrentSubject() {
|
void globalAdminShouldFindOnlyOwnProfileByCurrentSubject() {
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundCredentials = attempt(
|
final var foundProfile = attempt(
|
||||||
em, () -> {
|
em, () -> {
|
||||||
context(SUPERUSER_FRAN_SUBJECT_NAME);
|
context(SUPERUSER_FRAN_SUBJECT_NAME);
|
||||||
return credentialsRepository.findByCurrentSubject();
|
return profileRepository.findByCurrentSubject();
|
||||||
})
|
})
|
||||||
.assertNotNull().returnedValue();
|
.assertNotNull().returnedValue();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundCredentials).hasSize(1)
|
assertThat(foundProfile).hasSize(1)
|
||||||
.map(HsCredentialsEntity::getEmailAddress)
|
.map(HsProfileEntity::getEmailAddress)
|
||||||
.containsExactlyInAnyOrder("fran@example.com");
|
.containsExactlyInAnyOrder("fran@example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindByUuidUsingTestData() {
|
void shouldFindByUuidUsingTestData() {
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid());
|
final var foundEntityOptional = profileRepository.findByUuid(alexSubject.getUuid());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent()
|
assertThat(foundEntityOptional).isPresent()
|
||||||
.map(HsCredentialsEntity::getEmailAddress).contains("alex@example.com");
|
.map(HsProfileEntity::getEmailAddress).contains("alex@example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveCredentialsWithExistingContext() {
|
void shouldSaveProfileWithExistingScope() {
|
||||||
// given
|
// given
|
||||||
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
final var existingScope = scopeRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
final var newCredentials = HsCredentialsEntity.builder()
|
final var newProfile = HsProfileEntity.builder()
|
||||||
.subject(testUserSubject)
|
.subject(testUserSubject)
|
||||||
.person(testUserPerson)
|
.person(testUserPerson)
|
||||||
.active(true)
|
.active(true)
|
||||||
.emailAddress("test-user@example.com")
|
.emailAddress("test-user@example.com")
|
||||||
.globalUid(2011)
|
.globalUid(2011)
|
||||||
.globalGid(2011)
|
.globalGid(2011)
|
||||||
.loginContexts(mutableSetOf(existingContext))
|
.scopes(mutableSetOf(existingScope))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
toCleanup(credentialsRepository.save(newCredentials));
|
toCleanup(profileRepository.save(newProfile));
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid());
|
final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid());
|
||||||
assertThat(foundEntityOptional).isPresent();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("test-user@example.com");
|
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.getVersion()).isEqualTo(0); // Initial version
|
||||||
assertThat(foundEntity.getGlobalUid()).isEqualTo(2011);
|
assertThat(foundEntity.getGlobalUid()).isEqualTo(2011);
|
||||||
|
|
||||||
assertThat(foundEntity.getLoginContexts()).hasSize(1)
|
assertThat(foundEntity.getScopes()).hasSize(1)
|
||||||
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
.map(HsProfileScopeRealEntity::toString).contains("scope(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldNotSaveCredentialsWithNewContext() {
|
void shouldNotSaveProfileWithNewScope() {
|
||||||
// given
|
// given
|
||||||
final var newContext = HsCredentialsContextRealEntity.builder()
|
final var newScope = HsProfileScopeRealEntity.builder()
|
||||||
.type("MATRIX")
|
.type("MATRIX")
|
||||||
.qualifier("forbidden")
|
.qualifier("forbidden")
|
||||||
.build();
|
.build();
|
||||||
final var newCredentials = HsCredentialsEntity.builder()
|
final var newProfile = HsProfileEntity.builder()
|
||||||
.subject(drewSubject)
|
.subject(drewSubject)
|
||||||
.active(true)
|
.active(true)
|
||||||
.emailAddress("drew.new@example.com")
|
.emailAddress("drew.new@example.com")
|
||||||
.globalUid(2001)
|
.globalUid(2001)
|
||||||
.globalGid(2001)
|
.globalGid(2001)
|
||||||
.loginContexts(mutableSetOf(newContext))
|
.scopes(mutableSetOf(newScope))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var exception = catchThrowable(() -> {
|
final var exception = catchThrowable(() -> {
|
||||||
credentialsRepository.save(newCredentials);
|
profileRepository.save(newProfile);
|
||||||
em.flush();
|
em.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -213,9 +213,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveNewCredentialsWithoutContext() {
|
void shouldSaveNewProfileWithoutScope() {
|
||||||
// given
|
// given
|
||||||
final var newCredentials = HsCredentialsEntity.builder()
|
final var newProfile = HsProfileEntity.builder()
|
||||||
.subject(testUserSubject)
|
.subject(testUserSubject)
|
||||||
.person(testUserPerson)
|
.person(testUserPerson)
|
||||||
.active(true)
|
.active(true)
|
||||||
@@ -225,37 +225,37 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
credentialsRepository.save(newCredentials);
|
profileRepository.save(newProfile);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid());
|
final var foundEntityOptional = profileRepository.findByUuid(testUserSubject.getUuid());
|
||||||
assertThat(foundEntityOptional).isPresent();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com");
|
assertThat(foundEntity.getEmailAddress()).isEqualTo("test.user.new@example.com");
|
||||||
assertThat(foundEntity.isActive()).isTrue();
|
assertThat(foundEntity.isActive()).isTrue();
|
||||||
assertThat(foundEntity.getGlobalUid()).isEqualTo(20002);
|
assertThat(foundEntity.getGlobalUid()).isEqualTo(20002);
|
||||||
assertThat(foundEntity.getGlobalGid()).isEqualTo(2002);
|
assertThat(foundEntity.getGlobalGid()).isEqualTo(2002);
|
||||||
assertThat(foundEntity.getLoginContexts()).isEmpty();
|
assertThat(foundEntity.getScopes()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUpdateExistingCredentials() {
|
void shouldUpdateExistingProfile() {
|
||||||
// given
|
// given
|
||||||
final var entityToUpdate = credentialsRepository.findByUuid(alexSubject.getUuid()).orElseThrow();
|
final var entityToUpdate = profileRepository.findByUuid(alexSubject.getUuid()).orElseThrow();
|
||||||
final var initialVersion = entityToUpdate.getVersion();
|
final var initialVersion = entityToUpdate.getVersion();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
entityToUpdate.setActive(false);
|
entityToUpdate.setActive(false);
|
||||||
entityToUpdate.setEmailAddress("updated.user1@example.com");
|
entityToUpdate.setEmailAddress("updated.user1@example.com");
|
||||||
final var savedEntity = credentialsRepository.save(entityToUpdate);
|
final var savedEntity = profileRepository.save(entityToUpdate);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion);
|
assertThat(savedEntity.getVersion()).isGreaterThan(initialVersion);
|
||||||
final var updatedEntityOptional = credentialsRepository.findByUuid(alexSubject.getUuid());
|
final var updatedEntityOptional = profileRepository.findByUuid(alexSubject.getUuid());
|
||||||
assertThat(updatedEntityOptional).isPresent();
|
assertThat(updatedEntityOptional).isPresent();
|
||||||
final var updatedEntity = updatedEntityOptional.get();
|
final var updatedEntity = updatedEntityOptional.get();
|
||||||
assertThat(updatedEntity.isActive()).isFalse();
|
assertThat(updatedEntity.isActive()).isFalse();
|
||||||
@@ -307,8 +307,8 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
return new RelationBuilder(relationType);
|
return new RelationBuilder(relationType);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CredentialsBuilder givenCredentials() {
|
private ProfileBuilder givenProfile() {
|
||||||
return new CredentialsBuilder();
|
return new ProfileBuilder();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RelationBuilder {
|
private class RelationBuilder {
|
||||||
@@ -349,11 +349,11 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CredentialsBuilder {
|
private class ProfileBuilder {
|
||||||
private RbacSubjectEntity subject;
|
private RbacSubjectEntity subject;
|
||||||
private HsOfficePersonRealEntity person;
|
private HsOfficePersonRealEntity person;
|
||||||
|
|
||||||
public CredentialsBuilder forSubject(String subjectName) {
|
public ProfileBuilder forSubject(String subjectName) {
|
||||||
this.subject = RbacSubjectEntity.builder()
|
this.subject = RbacSubjectEntity.builder()
|
||||||
.name(subjectName)
|
.name(subjectName)
|
||||||
.build();
|
.build();
|
||||||
@@ -362,24 +362,24 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CredentialsBuilder forPerson(HsOfficePersonRealEntity person) {
|
public ProfileBuilder forPerson(HsOfficePersonRealEntity person) {
|
||||||
this.person = person;
|
this.person = person;
|
||||||
return this;
|
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())
|
.uuid(subject.getUuid())
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
||||||
.emailAddress(emailAddress)
|
.emailAddress(emailAddress)
|
||||||
.active(true)
|
.active(true)
|
||||||
.build();
|
.build();
|
||||||
em.persist(credentials);
|
em.persist(profile);
|
||||||
toCleanup(credentials);
|
toCleanup(profile);
|
||||||
em.flush();
|
em.flush();
|
||||||
return credentials;
|
return profile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+19
-19
@@ -38,13 +38,13 @@ import jakarta.persistence.EntityManager;
|
|||||||
import jakarta.persistence.EntityManagerFactory;
|
import jakarta.persistence.EntityManagerFactory;
|
||||||
import jakarta.persistence.SynchronizationType;
|
import jakarta.persistence.SynchronizationType;
|
||||||
|
|
||||||
@WebMvcTest(HsCredentialsContextsController.class)
|
@WebMvcTest(HsProfileScopeController.class)
|
||||||
@Import({ StrictMapper.class,
|
@Import({ StrictMapper.class,
|
||||||
MessageTranslator.class,
|
MessageTranslator.class,
|
||||||
JsonObjectMapperConfiguration.class,
|
JsonObjectMapperConfiguration.class,
|
||||||
WebSecurityConfigForWebMvcTests.class })
|
WebSecurityConfigForWebMvcTests.class })
|
||||||
@ActiveProfiles({"fake-jwt", "test"})
|
@ActiveProfiles({"fake-jwt", "test"})
|
||||||
class HsCredentialsContextsControllerRestTest {
|
class HsProfileScopeControllerRestTest {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
MockMvc mockMvc;
|
MockMvc mockMvc;
|
||||||
@@ -63,7 +63,7 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
EntityManagerFactory emf;
|
EntityManagerFactory emf;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
HsProfileScopeRbacRepository scopeRbacRepo;
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
public static class TestConfig {
|
public static class TestConfig {
|
||||||
@@ -82,14 +82,14 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getListOfLoginContextsReturnsOkWithEmptyList() throws Exception {
|
void getListOfScopesReturnsOkWithEmptyList() throws Exception {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
givenNoContextsInTheRepository();
|
givenNoScopesInTheRepository();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/hs/accounts/contexts")
|
.get("/api/hs/accounts/scopes")
|
||||||
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
.header("Authorization", bearer("superuser-alex@hostsharing.net"))
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
.andDo(print())
|
.andDo(print())
|
||||||
@@ -101,15 +101,15 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getListOfLoginContextsReturnsAllContextsForGlobalAdmin() throws Exception {
|
void getListOfScopesReturnsAllScopesForGlobalAdmin() throws Exception {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
givenSomeContextsInTheRepository();
|
givenSomeScopesInTheRepository();
|
||||||
when(contextMock.isGlobalAdmin()).thenReturn(true);
|
when(contextMock.isGlobalAdmin()).thenReturn(true);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/hs/accounts/contexts")
|
.get("/api/hs/accounts/scopes")
|
||||||
.header("Authorization", bearer("Bearer superuser-alex@hostsharing.net"))
|
.header("Authorization", bearer("Bearer superuser-alex@hostsharing.net"))
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
.andDo(print())
|
.andDo(print())
|
||||||
@@ -143,15 +143,15 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getListOfLoginContextsReturnsOnlyPublicContextsForNormalUser() throws Exception {
|
void getListOfScopesReturnsOnlyPublicScopesForNormalUser() throws Exception {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
givenSomeContextsInTheRepository();
|
givenSomeScopesInTheRepository();
|
||||||
when(contextMock.isGlobalAdmin()).thenReturn(false);
|
when(contextMock.isGlobalAdmin()).thenReturn(false);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.get("/api/hs/accounts/contexts")
|
.get("/api/hs/accounts/scopes")
|
||||||
.header("Authorization", bearer("drew@hostsharing.org"))
|
.header("Authorization", bearer("drew@hostsharing.org"))
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
.andDo(print())
|
.andDo(print())
|
||||||
@@ -178,27 +178,27 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void givenNoContextsInTheRepository() {
|
private void givenNoScopesInTheRepository() {
|
||||||
when(loginContextRbacRepo.findAll()).thenReturn(emptyList());
|
when(scopeRbacRepo.findAll()).thenReturn(emptyList());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void givenSomeContextsInTheRepository() {
|
private void givenSomeScopesInTheRepository() {
|
||||||
when(loginContextRbacRepo.findAll()).thenReturn(List.of(
|
when(scopeRbacRepo.findAll()).thenReturn(List.of(
|
||||||
HsCredentialsContextRbacEntity.builder()
|
HsProfileScopeRbacEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("HSADMIN")
|
.type("HSADMIN")
|
||||||
.qualifier("prod")
|
.qualifier("prod")
|
||||||
.publicAccess(true)
|
.publicAccess(true)
|
||||||
.onlyForNaturalPersons(true)
|
.onlyForNaturalPersons(true)
|
||||||
.build(),
|
.build(),
|
||||||
HsCredentialsContextRbacEntity.builder()
|
HsProfileScopeRbacEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("public")
|
.qualifier("public")
|
||||||
.publicAccess(true)
|
.publicAccess(true)
|
||||||
.onlyForNaturalPersons(false)
|
.onlyForNaturalPersons(false)
|
||||||
.build(),
|
.build(),
|
||||||
HsCredentialsContextRbacEntity.builder()
|
HsProfileScopeRbacEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("internal")
|
.qualifier("internal")
|
||||||
+4
-4
@@ -6,11 +6,11 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
class HsCredentialsContextRbacEntityUnitTest {
|
class HsProfileScopeRbacEntityUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toShortStringContainsJustTypeAndQualifier() {
|
void toShortStringContainsJustTypeAndQualifier() {
|
||||||
final var entity = HsCredentialsContextRbacEntity.builder()
|
final var entity = HsProfileScopeRbacEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("prod")
|
.qualifier("prod")
|
||||||
@@ -21,12 +21,12 @@ class HsCredentialsContextRbacEntityUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toStringContainsAllNonNullFields() {
|
void toStringContainsAllNonNullFields() {
|
||||||
final var entity = HsCredentialsContextRbacEntity.builder()
|
final var entity = HsProfileScopeRbacEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("prod")
|
.qualifier("prod")
|
||||||
.publicAccess(true)
|
.publicAccess(true)
|
||||||
.build();
|
.build();
|
||||||
assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString());
|
assertEquals("scope(SSH:prod:PUBLIC)", entity.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+27
-27
@@ -25,11 +25,11 @@ import static org.assertj.core.api.Assertions.catchThrowable;
|
|||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
@Import({ Context.class, JpaAttempt.class })
|
@Import({ Context.class, JpaAttempt.class })
|
||||||
@Transactional
|
@Transactional
|
||||||
class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest {
|
class HsProfileScopeRbacRepositoryIntegrationTest extends ContextBasedTest {
|
||||||
|
|
||||||
// existing UUIDs from test data (Liquibase changeset 310-login-credentials-test-data.sql)
|
// existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql)
|
||||||
private static final UUID TEST_CONTEXT_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
private static final UUID TEST_SCOPE_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");
|
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 SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||||
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
||||||
@@ -38,21 +38,21 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsContextRbacRepository loginContextRepository;
|
private HsProfileScopeRbacRepository scopesRepository;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldFindAllByNormalUserUsingTestData() {
|
void shouldFindAllByNormalUserUsingTestData() {
|
||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var allContexts = loginContextRepository.findAll();
|
final var allScopes = scopesRepository.findAll();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(allContexts)
|
assertThat(allScopes)
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.hasSizeGreaterThanOrEqualTo(1) // Expect at least the 1 public context from assumed test data
|
.hasSizeGreaterThanOrEqualTo(1) // Expect at least the 1 public context from assumed test data
|
||||||
.extracting(HsCredentialsContext::getUuid)
|
.extracting(HsProfileScope::getUuid)
|
||||||
.contains(TEST_CONTEXT_HSADMIN_PROD_UUID);
|
.contains(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -60,12 +60,12 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var allContexts = loginContextRepository.findAll();
|
final var allScopes = scopesRepository.findAll();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(allContexts)
|
assertThat(allScopes)
|
||||||
.isNotNull()
|
.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
|
@Test
|
||||||
@@ -73,11 +73,11 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByUuid(TEST_CONTEXT_HSADMIN_PROD_UUID);
|
final var foundEntityOptional = scopesRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent();
|
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
|
@Test
|
||||||
@@ -85,11 +85,11 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier("SSH", "internal");
|
final var foundEntityOptional = scopesRepository.findByTypeAndQualifier("SSH", "internal");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent();
|
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
|
@Test
|
||||||
@@ -100,7 +100,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
final var nonExistentQualifier = "non-existent-qualifier";
|
final var nonExistentQualifier = "non-existent-qualifier";
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier(
|
final var foundEntityOptional = scopesRepository.findByTypeAndQualifier(
|
||||||
"HSADMIN", nonExistentQualifier);
|
"HSADMIN", nonExistentQualifier);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@@ -108,19 +108,19 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveNewLoginContext() {
|
void shouldSaveNewScope() {
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
|
|
||||||
// given
|
// given
|
||||||
final var newQualifier = "test@example.social";
|
final var newQualifier = "test@example.social";
|
||||||
final var newType = "MASTODON";
|
final var newType = "MASTODON";
|
||||||
final var newContext = HsCredentialsContextRbacEntity.builder()
|
final var newScope = HsProfileScopeRbacEntity.builder()
|
||||||
.type(newType)
|
.type(newType)
|
||||||
.qualifier(newQualifier)
|
.qualifier(newQualifier)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var savedEntity = loginContextRepository.save(newContext);
|
final var savedEntity = scopesRepository.save(newScope);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
@@ -131,7 +131,7 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
|
|
||||||
// Fetch again using the generated UUID to confirm persistence
|
// Fetch again using the generated UUID to confirm persistence
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME); // Re-set context if needed after clear
|
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();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
||||||
@@ -140,21 +140,21 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPreventUpdateOfExistingLoginContext() {
|
void shouldPreventUpdateOfExistingScope() {
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
|
|
||||||
// given an existing entity from test data
|
// 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)
|
assertThat(entityToUpdateOptional)
|
||||||
.withFailMessage("Could not find existing LoginContext with UUID %s. Ensure test data exists.",
|
.withFailMessage("Could not find existing scope with UUID %s. Ensure test data exists.",
|
||||||
TEST_CONTEXT_MATRIX_INTERNAL_UUID)
|
TEST_SCOPE_MATRIX_INTERNAL_UUID)
|
||||||
.isPresent();
|
.isPresent();
|
||||||
final var entityToUpdate = entityToUpdateOptional.get();
|
final var entityToUpdate = entityToUpdateOptional.get();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
entityToUpdate.setQualifier("updated");
|
entityToUpdate.setQualifier("updated");
|
||||||
final var exception = catchThrowable( () -> {
|
final var exception = catchThrowable( () -> {
|
||||||
loginContextRepository.save(entityToUpdate);
|
scopesRepository.save(entityToUpdate);
|
||||||
em.flush();
|
em.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -162,6 +162,6 @@ class HsCredentialsContextRbacRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
assertThat(exception)
|
assertThat(exception)
|
||||||
.isInstanceOf(PersistenceException.class)
|
.isInstanceOf(PersistenceException.class)
|
||||||
.hasCauseInstanceOf(PSQLException.class)
|
.hasCauseInstanceOf(PSQLException.class)
|
||||||
.hasMessageContaining("ERROR: Updates to hs_accounts.context are not allowed.");
|
.hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-4
@@ -6,11 +6,11 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
class HsCredentialsContextRealEntityUnitTest {
|
class HsProfileScopeRealEntityUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toShortStringContainsJustTypeAndQualifier() {
|
void toShortStringContainsJustTypeAndQualifier() {
|
||||||
final var entity = HsCredentialsContextRealEntity.builder()
|
final var entity = HsProfileScopeRealEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("prod")
|
.qualifier("prod")
|
||||||
@@ -21,12 +21,12 @@ class HsCredentialsContextRealEntityUnitTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toStringContainsAllNonNullFields() {
|
void toStringContainsAllNonNullFields() {
|
||||||
final var entity = HsCredentialsContextRealEntity.builder()
|
final var entity = HsProfileScopeRealEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("SSH")
|
.type("SSH")
|
||||||
.qualifier("prod")
|
.qualifier("prod")
|
||||||
.publicAccess(true)
|
.publicAccess(true)
|
||||||
.build();
|
.build();
|
||||||
assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString());
|
assertEquals("scope(SSH:prod:PUBLIC)", entity.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+28
-28
@@ -25,12 +25,12 @@ import static org.assertj.core.api.Assertions.catchThrowable;
|
|||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
@Import({ Context.class, JpaAttempt.class })
|
@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)
|
// existing UUIDs from test data (Liquibase changeset 310-login-profile-test-data.sql)
|
||||||
private static final UUID TEST_CONTEXT_HSADMIN_PROD_UUID = UUID.fromString("11111111-1111-1111-1111-111111111111");
|
private static final UUID TEST_SCOPE_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_SCOPE_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");
|
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 SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||||
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
||||||
@@ -39,12 +39,12 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsContextRealRepository loginContextRepository;
|
private HsProfileScopeRealRepository scopeRepository;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void historizationIsAvailable() {
|
public void historizationIsAvailable() {
|
||||||
// given
|
// given
|
||||||
final String nativeQuerySql = "select * from hs_accounts.context_hv";
|
final String nativeQuerySql = "select * from hs_accounts.scope_hv";
|
||||||
|
|
||||||
// when
|
// when
|
||||||
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
historicalContext(Timestamp.from(ZonedDateTime.now().minusDays(1).toInstant()));
|
||||||
@@ -53,7 +53,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsBefore)
|
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);
|
.hasSize(0);
|
||||||
|
|
||||||
// and when
|
// and when
|
||||||
@@ -63,7 +63,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsAfter)
|
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);
|
.hasSize(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +72,14 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var allContexts = loginContextRepository.findAll();
|
final var allScopes = scopeRepository.findAll();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(allContexts)
|
assertThat(allScopes)
|
||||||
.isNotNull()
|
.isNotNull()
|
||||||
.hasSizeGreaterThanOrEqualTo(3) // Expect at least the 3 from assumed test data
|
.hasSizeGreaterThanOrEqualTo(3) // Expect at least the 3 from assumed test data
|
||||||
.extracting(HsCredentialsContext::getUuid)
|
.extracting(HsProfileScope::getUuid)
|
||||||
.contains(TEST_CONTEXT_HSADMIN_PROD_UUID, TEST_CONTEXT_SSH_INTERNAL_UUID, TEST_CONTEXT_MATRIX_INTERNAL_UUID);
|
.contains(TEST_SCOPE_HSADMIN_PROD_UUID, TEST_SCOPE_SSH_INTERNAL_UUID, TEST_SCOPE_MATRIX_INTERNAL_UUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -87,11 +87,11 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByUuid(TEST_CONTEXT_HSADMIN_PROD_UUID);
|
final var foundEntityOptional = scopeRepository.findByUuid(TEST_SCOPE_HSADMIN_PROD_UUID);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent();
|
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
|
@Test
|
||||||
@@ -99,11 +99,11 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier("SSH", "internal");
|
final var foundEntityOptional = scopeRepository.findByTypeAndQualifier("SSH", "internal");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(foundEntityOptional).isPresent();
|
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
|
@Test
|
||||||
@@ -114,7 +114,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
final var nonExistentQualifier = "non-existent-qualifier";
|
final var nonExistentQualifier = "non-existent-qualifier";
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var foundEntityOptional = loginContextRepository.findByTypeAndQualifier(
|
final var foundEntityOptional = scopeRepository.findByTypeAndQualifier(
|
||||||
"HSADMIN", nonExistentQualifier);
|
"HSADMIN", nonExistentQualifier);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
@@ -122,19 +122,19 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldSaveNewLoginContext() {
|
void shouldSaveNewScope() {
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
|
|
||||||
// given
|
// given
|
||||||
final var newQualifier = "test@example.social";
|
final var newQualifier = "test@example.social";
|
||||||
final var newType = "MASTODON";
|
final var newType = "MASTODON";
|
||||||
final var newContext = HsCredentialsContextRealEntity.builder()
|
final var newScope = HsProfileScopeRealEntity.builder()
|
||||||
.type(newType)
|
.type(newType)
|
||||||
.qualifier(newQualifier)
|
.qualifier(newQualifier)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
final var savedEntity = loginContextRepository.save(newContext);
|
final var savedEntity = scopeRepository.save(newScope);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
|
|
||||||
// Fetch again using the generated UUID to confirm persistence
|
// Fetch again using the generated UUID to confirm persistence
|
||||||
context(TEST_USER_SUBJECT_NAME); // Re-set context if needed after clear
|
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();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
assertThat(foundEntity.getUuid()).isEqualTo(generatedUuid);
|
||||||
@@ -154,21 +154,21 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPreventUpdateOfExistingLoginContext() {
|
void shouldPreventUpdateOfExistingScope() {
|
||||||
context(TEST_USER_SUBJECT_NAME);
|
context(TEST_USER_SUBJECT_NAME);
|
||||||
|
|
||||||
// given an existing entity from test data
|
// 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)
|
assertThat(entityToUpdateOptional)
|
||||||
.withFailMessage("Could not find existing LoginContext with UUID %s. Ensure test data exists.",
|
.withFailMessage("Could not find existing Scope with UUID %s. Ensure test data exists.",
|
||||||
TEST_CONTEXT_MATRIX_INTERNAL_UUID)
|
TEST_SCOPE_MATRIX_INTERNAL_UUID)
|
||||||
.isPresent();
|
.isPresent();
|
||||||
final var entityToUpdate = entityToUpdateOptional.get();
|
final var entityToUpdate = entityToUpdateOptional.get();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
entityToUpdate.setQualifier("updated");
|
entityToUpdate.setQualifier("updated");
|
||||||
final var exception = catchThrowable( () -> {
|
final var exception = catchThrowable( () -> {
|
||||||
loginContextRepository.save(entityToUpdate);
|
scopeRepository.save(entityToUpdate);
|
||||||
em.flush();
|
em.flush();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -176,6 +176,6 @@ class HsCredentialsContextRealRepositoryIntegrationTest extends ContextBasedTest
|
|||||||
assertThat(exception)
|
assertThat(exception)
|
||||||
.isInstanceOf(PersistenceException.class)
|
.isInstanceOf(PersistenceException.class)
|
||||||
.hasCauseInstanceOf(PSQLException.class)
|
.hasCauseInstanceOf(PSQLException.class)
|
||||||
.hasMessageContaining("ERROR: Updates to hs_accounts.context are not allowed.");
|
.hasMessageContaining("ERROR: Updates to hs_accounts.scope are not allowed.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-38
@@ -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<T extends UseCase<?>> extends UseCase<T> {
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<T extends UseCase<?>> extends UseCase<T> {
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-15
@@ -8,12 +8,12 @@ import org.springframework.http.HttpStatus;
|
|||||||
import static io.restassured.http.ContentType.JSON;
|
import static io.restassured.http.ContentType.JSON;
|
||||||
import static org.springframework.http.HttpStatus.OK;
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials> {
|
public class CreateProfile extends BaseProfileUseCase<CreateProfile> {
|
||||||
|
|
||||||
public CreateCredentials(final ScenarioTest testSuite) {
|
public CreateProfile(final ScenarioTest testSuite) {
|
||||||
super(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
|
@Override
|
||||||
@@ -26,12 +26,12 @@ public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials>
|
|||||||
"In real situations we have more precise measures to find the related person."
|
"In real situations we have more precise measures to find the related person."
|
||||||
);
|
);
|
||||||
|
|
||||||
given("resolvedContexts",
|
given("resolvedScopes",
|
||||||
fetchContextResourcesByDescriptorPairs("contexts")
|
fetchScopeResourcesByDescriptorPairs("scopes")
|
||||||
);
|
);
|
||||||
|
|
||||||
return obtain("newCredentials", () ->
|
return obtain("newProfile", () ->
|
||||||
httpPost("/api/hs/accounts/credentials", usingJsonBody("""
|
httpPost("/api/hs/accounts/profiles", usingJsonBody("""
|
||||||
{
|
{
|
||||||
"person.uuid": ${Person: %{personGivenName} %{personFamilyName}},
|
"person.uuid": ${Person: %{personGivenName} %{personFamilyName}},
|
||||||
"nickname": ${nickname},
|
"nickname": ${nickname},
|
||||||
@@ -40,10 +40,9 @@ public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials>
|
|||||||
"emailAddress": ${emailAddress},
|
"emailAddress": ${emailAddress},
|
||||||
"phonePassword": ${phonePassword},
|
"phonePassword": ${phonePassword},
|
||||||
"smsNumber": ${smsNumber},
|
"smsNumber": ${smsNumber},
|
||||||
"onboardingToken": ${onboardingToken},
|
|
||||||
"globalUid": %{globalUid},
|
"globalUid": %{globalUid},
|
||||||
"globalGid": %{globalGid},
|
"globalGid": %{globalGid},
|
||||||
"contexts": @{resolvedContexts}
|
"scopes": @{resolvedScopes}
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||||
@@ -51,16 +50,15 @@ public class CreateCredentials extends BaseCredentialsUseCase<CreateCredentials>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void verify(final UseCase<CreateCredentials>.HttpResponse response) {
|
protected void verify(final UseCase<CreateProfile>.HttpResponse response) {
|
||||||
verify(
|
verify(
|
||||||
"Verify the New Credentials",
|
"Verify the new Profile",
|
||||||
() -> httpGet("/api/hs/accounts/credentials/%{newCredentials}")
|
() -> httpGet("/api/hs/accounts/profiles/%{newProfile}")
|
||||||
.expecting(OK).expecting(JSON),
|
.expecting(OK).expecting(JSON),
|
||||||
path("uuid").contains("%{newCredentials}"),
|
path("uuid").contains("%{newProfile}"),
|
||||||
path("nickname").contains("%{nickname}"),
|
path("nickname").contains("%{nickname}"),
|
||||||
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"),
|
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}"),
|
||||||
path("totpSecrets").contains("@{totpSecrets}"),
|
path("totpSecrets").contains("@{totpSecrets}")
|
||||||
path("onboardingToken").contains("%{onboardingToken}")
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+14
-15
@@ -14,7 +14,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.junit.jupiter.api.TestInfo;
|
import org.junit.jupiter.api.TestInfo;
|
||||||
import org.junit.jupiter.api.TestMethodOrder;
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
|
||||||
class CredentialsScenarioTests extends ScenarioTest {
|
class ProfileScenarioTests extends ScenarioTest {
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -61,19 +61,19 @@ class CredentialsScenarioTests extends ScenarioTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@Order(30)
|
@Order(30)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
class CredentialScenarios {
|
class ProfileScenarios {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(1010)
|
@Order(1010)
|
||||||
@Produces(explicitly = "Credentials@hsadmin: firby-susan", implicitly = { "Person: Susan Firby" })
|
@Produces(explicitly = "Profile: firby-susan", implicitly = { "Person: Susan Firby" })
|
||||||
void shouldCreateInitialCredentialsForExistingNaturalPerson() {
|
void shouldCreateInitialProfileForExistingNaturalPerson() {
|
||||||
new CreateCredentials(scenarioTest)
|
new CreateProfile(scenarioTest)
|
||||||
// to find a specific existing person
|
// to find a specific existing person
|
||||||
.given("personFamilyName", "Firby")
|
.given("personFamilyName", "Firby")
|
||||||
.given("personGivenName", "Susan")
|
.given("personGivenName", "Susan")
|
||||||
// a login name, to be stored in the new RBAC subject
|
// a login name, to be stored in the new RBAC subject
|
||||||
.given("nickname", "firby-susan")
|
.given("nickname", "firby-susan")
|
||||||
// initial credentials
|
// initial profile
|
||||||
.given("active", true)
|
.given("active", true)
|
||||||
.given("totpSecrets", Array.of("initialSecret"))
|
.given("totpSecrets", Array.of("initialSecret"))
|
||||||
.given("emailAddress", "susan.firby@example.com")
|
.given("emailAddress", "susan.firby@example.com")
|
||||||
@@ -82,29 +82,28 @@ class CredentialsScenarioTests extends ScenarioTest {
|
|||||||
.given("globalUid", 21011)
|
.given("globalUid", 21011)
|
||||||
.given("globalGid", 21011)
|
.given("globalGid", 21011)
|
||||||
.given(
|
.given(
|
||||||
"contexts", Array.of(
|
"scopes", Array.of(
|
||||||
Pair.of("HSADMIN", "prod")
|
Pair.of("HSADMIN", "prod")
|
||||||
))
|
))
|
||||||
.given("onboardingToken", "fake-unboarding-token")
|
|
||||||
.doRun()
|
.doRun()
|
||||||
.keep();
|
.keep();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Order(1020)
|
@Order(1020)
|
||||||
@Requires("Credentials@hsadmin: firby-susan")
|
@Requires("Profile: firby-susan")
|
||||||
void shouldUpdateCredentials() {
|
void shouldUpdateProfile() {
|
||||||
new UpdateCredentials(scenarioTest)
|
new UpdateProfile(scenarioTest)
|
||||||
// the credentials to update
|
// the profile to update
|
||||||
.given("credentialsUuid", "%{Credentials@hsadmin: firby-susan}")
|
.given("profileUuid", "%{Profile: firby-susan}")
|
||||||
// updated credentials
|
// updated profile
|
||||||
.given("active", false)
|
.given("active", false)
|
||||||
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
.given("totpSecrets", Array.of("initialSecret", "additionalSecret"))
|
||||||
.given("emailAddress", "susan.firby@example.org")
|
.given("emailAddress", "susan.firby@example.org")
|
||||||
.given("phonePassword", "securePass987")
|
.given("phonePassword", "securePass987")
|
||||||
.given("smsNumber", "+49987654321")
|
.given("smsNumber", "+49987654321")
|
||||||
.given(
|
.given(
|
||||||
"contexts", Array.of(
|
"scopes", Array.of(
|
||||||
Pair.of("HSADMIN", "prod"),
|
Pair.of("HSADMIN", "prod"),
|
||||||
Pair.of("SSH", "internal")
|
Pair.of("SSH", "internal")
|
||||||
))
|
))
|
||||||
+12
-12
@@ -8,30 +8,30 @@ import org.springframework.http.HttpStatus;
|
|||||||
import static io.restassured.http.ContentType.JSON;
|
import static io.restassured.http.ContentType.JSON;
|
||||||
import static org.springframework.http.HttpStatus.OK;
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
public class UpdateCredentials extends BaseCredentialsUseCase<UpdateCredentials> {
|
public class UpdateProfile extends BaseProfileUseCase<UpdateProfile> {
|
||||||
|
|
||||||
public UpdateCredentials(final ScenarioTest testSuite) {
|
public UpdateProfile(final ScenarioTest testSuite) {
|
||||||
super(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
|
@Override
|
||||||
protected HttpResponse run() {
|
protected HttpResponse run() {
|
||||||
|
|
||||||
given("resolvedContexts",
|
given("resolvedScopes",
|
||||||
fetchContextResourcesByDescriptorPairs("contexts")
|
fetchScopeResourcesByDescriptorPairs("scopes")
|
||||||
);
|
);
|
||||||
|
|
||||||
withTitle("Patch the Changes to the existing Credentials", () ->
|
withTitle("Patch the Changes to the existing Profile", () ->
|
||||||
httpPatch("/api/hs/accounts/credentials/%{credentialsUuid}", usingJsonBody("""
|
httpPatch("/api/hs/accounts/profiles/%{profileUuid}", usingJsonBody("""
|
||||||
{
|
{
|
||||||
"active": %{active},
|
"active": %{active},
|
||||||
"totpSecrets": @{totpSecrets},
|
"totpSecrets": @{totpSecrets},
|
||||||
"emailAddress": ${emailAddress},
|
"emailAddress": ${emailAddress},
|
||||||
"phonePassword": ${phonePassword},
|
"phonePassword": ${phonePassword},
|
||||||
"smsNumber": ${smsNumber},
|
"smsNumber": ${smsNumber},
|
||||||
"contexts": @{resolvedContexts}
|
"scopes": @{resolvedScopes}
|
||||||
}
|
}
|
||||||
"""))
|
"""))
|
||||||
.reportWithResponse().expecting(HttpStatus.OK).expecting(ContentType.JSON)
|
.reportWithResponse().expecting(HttpStatus.OK).expecting(ContentType.JSON)
|
||||||
@@ -43,12 +43,12 @@ public class UpdateCredentials extends BaseCredentialsUseCase<UpdateCredentials>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void verify(final UseCase<UpdateCredentials>.HttpResponse response) {
|
protected void verify(final UseCase<UpdateProfile>.HttpResponse response) {
|
||||||
verify(
|
verify(
|
||||||
"Verify the Patched Credentials",
|
"Verify the Patched Profile",
|
||||||
() -> httpGet("/api/hs/accounts/credentials/%{credentialsUuid}")
|
() -> httpGet("/api/hs/accounts/profiles/%{profileUuid}")
|
||||||
.expecting(OK).expecting(JSON),
|
.expecting(OK).expecting(JSON),
|
||||||
path("uuid").contains("%{newCredentials}"),
|
path("uuid").contains("%{newProfile}"),
|
||||||
path("nickname").contains("%{nickname}"),
|
path("nickname").contains("%{nickname}"),
|
||||||
path("totpSecrets").contains("%{totpSecrets}")
|
path("totpSecrets").contains("%{totpSecrets}")
|
||||||
);
|
);
|
||||||
+2
-3
@@ -7,7 +7,6 @@ import net.hostsharing.hsadminng.rbac.context.Context;
|
|||||||
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Disabled;
|
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.is;
|
||||||
import static org.hamcrest.Matchers.startsWith;
|
import static org.hamcrest.Matchers.startsWith;
|
||||||
|
|
||||||
@Transactional
|
|
||||||
@Tag("officeIntegrationTest")
|
@Tag("officeIntegrationTest")
|
||||||
@SpringBootTest(
|
@SpringBootTest(
|
||||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
classes = HsadminNgApplication.class)
|
classes = HsadminNgApplication.class)
|
||||||
@ActiveProfiles("fake-jwt")
|
@ActiveProfiles("fake-jwt")
|
||||||
|
@Transactional
|
||||||
class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
@LocalServerPort
|
@LocalServerPort
|
||||||
@@ -58,7 +57,7 @@ class HsOfficeBankAccountControllerAcceptanceTest extends ContextBasedTestWithCl
|
|||||||
class GetListOfBankAccounts {
|
class GetListOfBankAccounts {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void globalAdmin_withoutAssumedRoles_canViewAllBankAccounts_ifNoCriteriaGiven() throws JSONException {
|
void globalAdmin_withoutAssumedRoles_canViewAllBankAccounts_ifNoCriteriaGiven() {
|
||||||
|
|
||||||
RestAssured // @formatter:off
|
RestAssured // @formatter:off
|
||||||
.given()
|
.given()
|
||||||
|
|||||||
Reference in New Issue
Block a user