credentials validation (#194)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/194 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
+1
-1
@@ -60,7 +60,7 @@ public class CredentialContextResourceToEntityMapper {
|
|||||||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
||||||
throw new EntityNotFoundException(
|
throw new EntityNotFoundException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.existing-{0}-does-not-match-given-resource-{1}",
|
"credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}",
|
||||||
existingContextEntity, resource));
|
existingContextEntity, resource));
|
||||||
}
|
}
|
||||||
entities.add(existingContextEntity);
|
entities.add(existingContextEntity);
|
||||||
|
|||||||
@@ -61,9 +61,13 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
|
|||||||
@Column(name = "public_access")
|
@Column(name = "public_access")
|
||||||
private boolean publicAccess;
|
private boolean publicAccess;
|
||||||
|
|
||||||
|
public boolean isHsadminContext() {
|
||||||
|
return "HSADMIN".equals(type);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return toString();
|
return type + (qualifier != null ? ":" + qualifier : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+5
-1
@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import lombok.val;
|
||||||
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
import net.hostsharing.hsadminng.config.NoSecurityRequirement;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ContextsApi;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.ContextsApi;
|
||||||
@@ -37,7 +38,10 @@ public class HsCredentialsContextsController implements ContextsApi {
|
|||||||
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
|
if (SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
}
|
}
|
||||||
final var loginContexts = contextRepo.findAll();
|
val isGlobalAdmin = context.isGlobalAdmin();
|
||||||
|
final var loginContexts = contextRepo.findAll().stream().filter(
|
||||||
|
context -> context.isPublicAccess() || isGlobalAdmin
|
||||||
|
).toList();
|
||||||
final var result = mapper.mapList(loginContexts, ContextResource.class);
|
final var result = mapper.mapList(loginContexts, ContextResource.class);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ 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;
|
||||||
|
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 net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
||||||
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;
|
||||||
@@ -19,7 +21,7 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatc
|
|||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.HsOfficePersonResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePerson;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
@@ -62,7 +64,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
private MessageTranslator messageTranslator;
|
private MessageTranslator messageTranslator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePersonRbacRepository rbacPersonRepo;
|
private HsOfficePersonRealRepository realPersonRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepo;
|
private HsCredentialsRepository credentialsRepo;
|
||||||
@@ -77,11 +79,11 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
|
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
|
||||||
final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
val credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
||||||
if (credentialsEntity.isEmpty()) {
|
if (credentialsEntity.isEmpty()) {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
final var result = mapper.map(
|
val result = mapper.map(
|
||||||
credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
credentialsEntity.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
@@ -95,10 +97,10 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var credentials = personUuid == null
|
val credentials = personUuid == null
|
||||||
? credentialsRepo.findByCurrentSubject()
|
? credentialsRepo.findByCurrentSubject()
|
||||||
: findByPersonUuid(personUuid);
|
: findByPersonUuid(personUuid);
|
||||||
final var result = mapper.mapList(
|
val result = mapper.mapList(
|
||||||
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
@@ -112,22 +114,25 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
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
|
||||||
final var 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 credentials entity with the subject's UUID
|
||||||
final var newCredentialsEntity = mapper.map(
|
val newCredentialsEntity = mapper.map(
|
||||||
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
validate(newCredentialsEntity);
|
validateOnCreate(newCredentialsEntity);
|
||||||
newCredentialsEntity.setSubject(newlySavedSubject);
|
|
||||||
|
// switch to the new subject to get access to its own subject RBAC object
|
||||||
|
context.define("activate newly created self-service subject", null, body.getNickname(), null);
|
||||||
|
newCredentialsEntity.setSubject(em.merge(newlySavedSubject)); // attached to EM by the new subject
|
||||||
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
||||||
|
|
||||||
// return the new credentials as a resource
|
// return the new credentials as a resource
|
||||||
final var uri =
|
val uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/hs/accounts/credentials/{id}")
|
.path("/api/hs/accounts/credentials/{id}")
|
||||||
.buildAndExpand(newCredentialsEntity.getUuid())
|
.buildAndExpand(newCredentialsEntity.getUuid())
|
||||||
.toUri();
|
.toUri();
|
||||||
final var newCredentialsResource = mapper.map(
|
val newCredentialsResource = mapper.map(
|
||||||
newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.created(uri).body(newCredentialsResource);
|
return ResponseEntity.created(uri).body(newCredentialsResource);
|
||||||
}
|
}
|
||||||
@@ -137,8 +142,9 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
||||||
public ResponseEntity<Void> deleteCredentialsByUuid(final UUID credentialsUuid) {
|
public ResponseEntity<Void> deleteCredentialsByUuid(final UUID credentialsUuid) {
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
val credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
||||||
credentialsEntity.getLoginContexts().clear();
|
credentialsEntity.getLoginContexts().clear();
|
||||||
|
validateOnDelete(credentialsEntity);
|
||||||
em.flush();
|
em.flush();
|
||||||
em.remove(credentialsEntity);
|
em.remove(credentialsEntity);
|
||||||
em.remove(credentialsEntity.getSubject());
|
em.remove(credentialsEntity.getSubject());
|
||||||
@@ -154,12 +160,13 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
) {
|
) {
|
||||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
|
||||||
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
new HsCredentialsEntityPatcher(contextMapper, current).apply(body);
|
new HsCredentialsEntityPatcher(contextMapper, current).apply(body);
|
||||||
|
validateOnUpdate(current);
|
||||||
|
|
||||||
final var saved = credentialsRepo.save(current);
|
val saved = credentialsRepo.save(current);
|
||||||
final var mapped = mapper.map(
|
val mapped = mapper.map(
|
||||||
saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
@@ -173,56 +180,129 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
context.define();
|
context.define();
|
||||||
|
|
||||||
// fetch the data
|
// fetch the data
|
||||||
final var currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
final var currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
val currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
||||||
|
val person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
|
||||||
|
|
||||||
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
||||||
final var person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
|
|
||||||
|
|
||||||
// finally, return the result
|
// finally, return the result
|
||||||
final var result = currentLoginUserResponse(currentSubject, person, isGlobalAdmin);
|
val result = currentLoginUserResponse(currentSubject, person, isGlobalAdmin);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.credentialsUsed")
|
@Timed("app.credentials.credentials.credentialsUsed")
|
||||||
public ResponseEntity<CredentialsResource> credentialsUsed(
|
public ResponseEntity<CredentialsResource> credentialsUsed(final UUID credentialsUuid) {
|
||||||
final String assumedRoles,
|
context.define();
|
||||||
final UUID credentialsUuid) {
|
|
||||||
context.assumeRoles(assumedRoles);
|
|
||||||
|
|
||||||
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
val current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
current.setOnboardingToken(null);
|
current.setOnboardingToken(null);
|
||||||
current.setLastUsed(LocalDateTime.now());
|
current.setLastUsed(LocalDateTime.now());
|
||||||
|
|
||||||
final var saved = credentialsRepo.save(current);
|
val saved = credentialsRepo.save(current);
|
||||||
final var mapped = mapper.map(saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
val mapped = mapper.map(saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validate(final HsCredentialsEntity newCredentialsEntity) {
|
private void validateOnCreate(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
// the referenced person must be represented by currently logged in person
|
validateReferencedPersonToBeRepresentedByLoginUserPerson(newCredentialsEntity);
|
||||||
final var personUuid = newCredentialsEntity.getPerson().getUuid();
|
validateNormalUsersOnlyAccessPublicContexts(newCredentialsEntity);
|
||||||
final var representedPersonUuids = rbacPersonRepo.findPersonsRepresentedByPersonWithUuid(personUuid)
|
validateNaturalPersonRequirementOfContexts(newCredentialsEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOnUpdate(final HsCredentialsEntity current) {
|
||||||
|
validateNormalUsersOnlyAccessPublicContexts(current);
|
||||||
|
validateNaturalPersonRequirementOfContexts(current);
|
||||||
|
validateOwnHsadminCredentialsMustNotBeRemoved(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOnDelete(final HsCredentialsEntity credentialsEntity) {
|
||||||
|
validateOwnHsadminCredentialsMustNotBeRemoved(credentialsEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateReferencedPersonToBeRepresentedByLoginUserPerson(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
|
if (context.isGlobalAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
val referredPersonUuid = newCredentialsEntity.getPerson().getUuid();
|
||||||
|
val currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
|
val loginPersonUuid = credentialsRepo.findByUuid(currentSubjectUuid)
|
||||||
|
.map(HsCredentialsEntity::getPerson)
|
||||||
|
.map(HsOfficePerson::getUuid)
|
||||||
|
.orElseThrow();
|
||||||
|
val representedPersonUuids = realPersonRepo.findPersonsRepresentedByPersonWithUuid(loginPersonUuid)
|
||||||
.stream().map(HsOfficePerson::getUuid).toList();
|
.stream().map(HsOfficePerson::getUuid).toList();
|
||||||
if ( !representedPersonUuids.contains(personUuid)) {
|
if ( !representedPersonUuids.contains(referredPersonUuid)) {
|
||||||
throw new ValidationException(
|
throw new ValidationException(
|
||||||
messageTranslator.translate(
|
messageTranslator.translate(
|
||||||
"credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person",
|
"credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person",
|
||||||
personUuid));
|
loginPersonUuid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateNormalUsersOnlyAccessPublicContexts(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
|
val forbiddenContexts = newCredentialsEntity.getLoginContexts().stream()
|
||||||
|
.filter(c -> !c.isPublicAccess() && !context.isGlobalAdmin() )
|
||||||
|
.toList();
|
||||||
|
if (!forbiddenContexts.isEmpty()) {
|
||||||
|
throw new ValidationException(
|
||||||
|
messageTranslator.translate(
|
||||||
|
"credentials.access-denied-for-contexts-{0}",
|
||||||
|
toDisplay(forbiddenContexts)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateNaturalPersonRequirementOfContexts(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
|
if (newCredentialsEntity.getPerson().getPersonType().equals(HsOfficePersonType.NATURAL_PERSON)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
val contextsWhichRequireNaturalPerson = newCredentialsEntity.getLoginContexts().stream()
|
||||||
|
.filter(HsCredentialsContext::isOnlyForNaturalPersons)
|
||||||
|
.toList();
|
||||||
|
if (!contextsWhichRequireNaturalPerson.isEmpty()) {
|
||||||
|
throw new ValidationException(
|
||||||
|
messageTranslator.translate(
|
||||||
|
"credentials.context-requires-natural-person-{0}",
|
||||||
|
toDisplay(contextsWhichRequireNaturalPerson)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateOwnHsadminCredentialsMustNotBeRemoved(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
|
if (!newCredentialsEntity.getSubject().getUuid().equals(context.fetchCurrentSubjectUuid())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
val hsadminCredentialsContext = newCredentialsEntity.getLoginContexts().stream()
|
||||||
|
.filter(HsCredentialsContext::isHsadminContext)
|
||||||
|
.toList();
|
||||||
|
if (hsadminCredentialsContext.isEmpty()) {
|
||||||
|
throw new ValidationException(
|
||||||
|
messageTranslator.translate(
|
||||||
|
"credentials.own-hsadmin-credentials-must-not-be-removed"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toDisplay(final List<HsCredentialsContextRealEntity> contextsWhichRequireNaturalPerson) {
|
||||||
|
return contextsWhichRequireNaturalPerson.stream()
|
||||||
|
.map(HsCredentialsContext::toShortString)
|
||||||
|
.sorted()
|
||||||
|
.map(s -> "'" + s + "'")
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
|
||||||
private RbacSubjectEntity createSubject(final String nickname) {
|
private RbacSubjectEntity createSubject(final String nickname) {
|
||||||
final var newRbacSubject = subjectRepo.create(new RbacSubjectEntity(null, nickname));
|
val rbacSubjectEntity = new RbacSubjectEntity(null, nickname);
|
||||||
if(context.fetchCurrentSubject() == null) {
|
val newRbacSubject = subjectRepo.create(rbacSubjectEntity);
|
||||||
context.define("activate newly created self-service subject", null, nickname, null);
|
return newRbacSubject;
|
||||||
}
|
|
||||||
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // now attached to EM
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
||||||
final var person = rbacPersonRepo.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)
|
||||||
)
|
)
|
||||||
@@ -236,7 +316,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
final RbacSubjectEntity currentSubject,
|
final RbacSubjectEntity currentSubject,
|
||||||
final HsOfficePerson<?> person,
|
final HsOfficePerson<?> person,
|
||||||
final boolean isGlobalAdmin) {
|
final boolean isGlobalAdmin) {
|
||||||
final var result = new CurrentLoginUserResource();
|
val result = new CurrentLoginUserResource();
|
||||||
result.setSubject(mapper.map(currentSubject, RbacSubjectResource.class));
|
result.setSubject(mapper.map(currentSubject, RbacSubjectResource.class));
|
||||||
result.setPerson(mapper.map(person, HsOfficePersonResource.class));
|
result.setPerson(mapper.map(person, HsOfficePersonResource.class));
|
||||||
result.setGlobalAdmin(isGlobalAdmin);
|
result.setGlobalAdmin(isGlobalAdmin);
|
||||||
@@ -267,7 +347,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
final var person = rbacPersonRepo.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())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||||
import net.hostsharing.hsadminng.persistence.BaseEntity; // Assuming BaseEntity exists
|
import net.hostsharing.hsadminng.persistence.BaseEntity; // Assuming BaseEntity exists
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
import net.hostsharing.hsadminng.repr.Stringify;
|
import net.hostsharing.hsadminng.repr.Stringify;
|
||||||
@@ -46,7 +46,7 @@ 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 HsOfficePersonRbacEntity person;
|
private HsOfficePersonRealEntity person; // TODO.impl: add RBAC-Support to CredentialsEntity, see Story #
|
||||||
|
|
||||||
@Version
|
@Version
|
||||||
private int version;
|
private int version;
|
||||||
|
|||||||
+16
@@ -23,6 +23,22 @@ public interface HsOfficePersonRealRepository extends Repository<HsOfficePersonR
|
|||||||
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.real")
|
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.real")
|
||||||
List<HsOfficePersonRealEntity> findPersonByOptionalNameLike(String name);
|
List<HsOfficePersonRealEntity> findPersonByOptionalNameLike(String name);
|
||||||
|
|
||||||
|
@Query(value = """
|
||||||
|
WITH RECURSIVE
|
||||||
|
represented_persons AS (
|
||||||
|
SELECT relation.anchorUuid person_uuid
|
||||||
|
FROM hs_office.relation relation
|
||||||
|
WHERE relation.type = 'REPRESENTATIVE'
|
||||||
|
AND relation.holderUuid = :personUuid
|
||||||
|
)
|
||||||
|
SELECT person.*
|
||||||
|
FROM hs_office.person person
|
||||||
|
WHERE person.uuid IN (SELECT person_uuid FROM represented_persons)
|
||||||
|
OR person.uuid = :personUuid
|
||||||
|
""", nativeQuery = true)
|
||||||
|
@Timed("app.office.persons.repo.findRepresentedPersons.real")
|
||||||
|
List<HsOfficePersonRealEntity> findPersonsRepresentedByPersonWithUuid(UUID personUuid);
|
||||||
|
|
||||||
@Timed("app.office.persons.repo.save.real")
|
@Timed("app.office.persons.repo.save.real")
|
||||||
HsOfficePersonRealEntity save(final HsOfficePersonRealEntity entity);
|
HsOfficePersonRealEntity save(final HsOfficePersonRealEntity entity);
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class PingController implements TestApi {
|
|||||||
public ResponseEntity<String> pong() {
|
public ResponseEntity<String> pong() {
|
||||||
final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
|
final var userName = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
|
// HOWTO translate text with placeholders - also see in resource files i18n/messages_*.properties.
|
||||||
final var translatedMessage = messageTranslator.translate("ping {0} - in English", userName);
|
final var translatedMessage = messageTranslator.translate("test.ponged-{0}--in-your-language", userName);
|
||||||
return ResponseEntity.ok(translatedMessage + "\n");
|
return ResponseEntity.ok(translatedMessage + "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,8 +43,7 @@ public class RbacSubjectController implements RbacSubjectsApi {
|
|||||||
if (body.getUuid() == null) {
|
if (body.getUuid() == null) {
|
||||||
body.setUuid(UUID.randomUUID());
|
body.setUuid(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
final var saved = mapper.map(body, RbacSubjectEntity.class);
|
final var saved = rbacSubjectRepository.create(mapper.map(body, RbacSubjectEntity.class));
|
||||||
rbacSubjectRepository.create(saved);
|
|
||||||
final var uri =
|
final var uri =
|
||||||
MvcUriComponentsBuilder.fromController(getClass())
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
.path("/api/rbac/subjects/{id}")
|
.path("/api/rbac/subjects/{id}")
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ public interface RbacSubjectRepository extends Repository<RbacSubjectEntity, UUI
|
|||||||
rbacSubjectEntity.setUuid(UUID.randomUUID());
|
rbacSubjectEntity.setUuid(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
insert(rbacSubjectEntity);
|
insert(rbacSubjectEntity);
|
||||||
|
// RbacSubjectEntity binds to 'rbac.subject_rv',
|
||||||
|
// but the current user might not be allowed to read the newly created row from the restricted view,
|
||||||
|
// only the newly created subject (or a global admin) is allowed to read the new subject.
|
||||||
|
// Thus, the code which calls this method needs to switch the login user and fetch an attached entity.
|
||||||
return rbacSubjectEntity; // Not yet attached to EM!
|
return rbacSubjectEntity; // Not yet attached to EM!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ post:
|
|||||||
description: 'Is called when credentials got used for a login.'
|
description: 'Is called when credentials got used for a login.'
|
||||||
operationId: credentialsUsed
|
operationId: credentialsUsed
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
|
||||||
- name: credentialsUuid
|
- name: credentialsUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ general.{0}-{1}-not-found-or-not-accessible={0} "{1}" nicht gefunden oder nicht
|
|||||||
general.but-is=ist aber
|
general.but-is=ist aber
|
||||||
|
|
||||||
# credentials validations
|
# credentials validations
|
||||||
credentials.existing-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
||||||
credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
|
credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
|
||||||
|
credentials.access-denied-for-contexts-{0}=Kontext-Zugriff verweigert: {0}
|
||||||
|
credentials.context-requires-natural-person-{0}=Kontext verlangt eine natürliche Person: {0}
|
||||||
|
credentials.own-hsadmin-credentials-must-not-be-removed=die eigenen hsadmin-Credentials dürfen nicht entfernt werden
|
||||||
|
|
||||||
# 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}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ general.{0}-{1}-not-found-or-not-accessible={0} "{1}" not found or not accessibl
|
|||||||
general.but-is=but is
|
general.but-is=but is
|
||||||
|
|
||||||
# credentials validations
|
# credentials validations
|
||||||
credentials.existing-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
|
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}=existing {0} does not match given resource {1}
|
||||||
credentials.access-denied-person-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
|
credentials.access-denied-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
|
||||||
|
credentials.access-denied-for-contexts-{0}=context-access denied: {0}
|
||||||
|
credentials.context-requires-natural-person-{0}=context requires natural person: {0}
|
||||||
|
credentials.own-hsadmin-credentials-must-not-be-removed=own hsadmin-credentials must not be removed
|
||||||
|
|
||||||
# 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}
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ general.{0}-{1}-not-found-or-not-accessible={0} "{1}" non trouvé ou non accessi
|
|||||||
general.but-is=mais c'est
|
general.but-is=mais c'est
|
||||||
|
|
||||||
# credentials validations
|
# credentials validations
|
||||||
credentials.existing-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
|
credentials.existing-credentials-context-{0}-does-not-match-given-resource-{1}={0} existant ne correspond pas à la ressource donnée {1}
|
||||||
credentials.access-denied-person-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-to-person-with-uuid-{0}-not-represented-by-currently-logged-in-person=accès refusé : personUuid "{0}" non représenté par la personne actuellement connectée
|
||||||
|
credentials.access-denied-for-contexts-{0}=accès au contexte refusé : {0}
|
||||||
|
credentials.context-requires-natural-person-{0}=le contexte requiert une personne physique : {0}
|
||||||
|
credentials.own-hsadmin-credentials-must-not-be-removed=suppression des identifiants hsadmin propres interdite
|
||||||
|
|
||||||
# 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}
|
||||||
|
|||||||
+13
-2
@@ -9,13 +9,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
class HsCredentialsContextRbacEntityUnitTest {
|
class HsCredentialsContextRbacEntityUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toShortString() {
|
void toShortStringContainsJustTypeAndQualifier() {
|
||||||
final var entity = HsCredentialsContextRbacEntity.builder()
|
final var entity = HsCredentialsContextRbacEntity.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.toShortString());
|
assertEquals("SSH:prod", entity.toShortString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringContainsAllNonNullFields() {
|
||||||
|
final var entity = HsCredentialsContextRbacEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type("SSH")
|
||||||
|
.qualifier("prod")
|
||||||
|
.publicAccess(true)
|
||||||
|
.build();
|
||||||
|
assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-5
@@ -9,13 +9,24 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
|||||||
class HsCredentialsContextRealEntityUnitTest {
|
class HsCredentialsContextRealEntityUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toShortString() {
|
void toShortStringContainsJustTypeAndQualifier() {
|
||||||
final var entity = HsCredentialsContextRealEntity.builder()
|
final var entity = HsCredentialsContextRealEntity.builder()
|
||||||
.uuid(UUID.randomUUID())
|
.uuid(UUID.randomUUID())
|
||||||
.type("testType")
|
.type("SSH")
|
||||||
.qualifier("testQualifier")
|
.qualifier("prod")
|
||||||
.onlyForNaturalPersons(true)
|
.publicAccess(true)
|
||||||
.build();
|
.build();
|
||||||
assertEquals("loginContext(testType:testQualifier:NP-ONLY:INTERNAL)", entity.toShortString());
|
assertEquals("SSH:prod", entity.toShortString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void toStringContainsAllNonNullFields() {
|
||||||
|
final var entity = HsCredentialsContextRealEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type("SSH")
|
||||||
|
.qualifier("prod")
|
||||||
|
.publicAccess(true)
|
||||||
|
.build();
|
||||||
|
assertEquals("loginContext(SSH:prod:PUBLIC)", entity.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+107
-24
@@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.accounts;
|
package net.hostsharing.hsadminng.hs.accounts;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -47,7 +48,7 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
Context contextMock;
|
Context contextMock;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@SuppressWarnings("unused") // not used in test, but in controller class
|
@SuppressWarnings("unused") // not used in test but in controller class
|
||||||
StrictMapper mapper;
|
StrictMapper mapper;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
@@ -59,15 +60,12 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
||||||
|
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
public static class TestConfig {
|
public static class TestConfig {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public EntityManager entityManager() {
|
public EntityManager entityManager() {
|
||||||
return mock(EntityManager.class);
|
return mock(EntityManager.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
@@ -82,18 +80,27 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
void getListOfLoginContextsReturnsOkWithEmptyList() throws Exception {
|
void getListOfLoginContextsReturnsOkWithEmptyList() throws Exception {
|
||||||
|
|
||||||
// given
|
// given
|
||||||
when(loginContextRbacRepo.findAll()).thenReturn(List.of(
|
givenNoContextsInTheRepository();
|
||||||
HsCredentialsContextRbacEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
// when
|
||||||
.type("HSADMIN")
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
.qualifier("prod")
|
.get("/api/hs/accounts/contexts")
|
||||||
.build(),
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
HsCredentialsContextRbacEntity.builder()
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
.uuid(UUID.randomUUID())
|
.andDo(print())
|
||||||
.type("SSH")
|
|
||||||
.qualifier("prod")
|
// then
|
||||||
.build()
|
.andExpect(status().isOk())
|
||||||
));
|
.andExpect(jsonPath("$").isArray())
|
||||||
|
.andExpect(jsonPath("$").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getListOfLoginContextsReturnsAllContextsForGlobalAdmin() throws Exception {
|
||||||
|
|
||||||
|
// given
|
||||||
|
givenSomeContextsInTheRepository();
|
||||||
|
when(contextMock.isGlobalAdmin()).thenReturn(true);
|
||||||
|
|
||||||
// when
|
// when
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
@@ -107,16 +114,92 @@ class HsCredentialsContextsControllerRestTest {
|
|||||||
.andExpect(jsonPath(
|
.andExpect(jsonPath(
|
||||||
"$", lenientlyEquals("""
|
"$", lenientlyEquals("""
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"type": "HSADMIN",
|
"type": "HSADMIN",
|
||||||
"qualifier": "prod"
|
"qualifier": "prod",
|
||||||
},
|
"onlyForNaturalPersons": true,
|
||||||
{
|
"publicAccess": true
|
||||||
"type": "SSH",
|
},
|
||||||
"qualifier": "prod"
|
{
|
||||||
}
|
"type": "SSH",
|
||||||
|
"qualifier": "public",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SSH",
|
||||||
|
"qualifier": "internal",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": false
|
||||||
|
}
|
||||||
]
|
]
|
||||||
"""
|
"""
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getListOfLoginContextsReturnsOnlyPublicContextsForNormalUser() throws Exception {
|
||||||
|
|
||||||
|
// given
|
||||||
|
givenSomeContextsInTheRepository();
|
||||||
|
when(contextMock.isGlobalAdmin()).thenReturn(false);
|
||||||
|
|
||||||
|
// when
|
||||||
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
|
.get("/api/hs/accounts/contexts")
|
||||||
|
.header("Authorization", "Bearer drew@hostsharing.org")
|
||||||
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
|
// then
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath(
|
||||||
|
"$", lenientlyEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "HSADMIN",
|
||||||
|
"qualifier": "prod",
|
||||||
|
"onlyForNaturalPersons": true,
|
||||||
|
"publicAccess": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SSH",
|
||||||
|
"qualifier": "public",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenNoContextsInTheRepository() {
|
||||||
|
when(loginContextRbacRepo.findAll()).thenReturn(emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void givenSomeContextsInTheRepository() {
|
||||||
|
when(loginContextRbacRepo.findAll()).thenReturn(List.of(
|
||||||
|
HsCredentialsContextRbacEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type("HSADMIN")
|
||||||
|
.qualifier("prod")
|
||||||
|
.publicAccess(true)
|
||||||
|
.onlyForNaturalPersons(true)
|
||||||
|
.build(),
|
||||||
|
HsCredentialsContextRbacEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type("SSH")
|
||||||
|
.qualifier("public")
|
||||||
|
.publicAccess(true)
|
||||||
|
.onlyForNaturalPersons(false)
|
||||||
|
.build(),
|
||||||
|
HsCredentialsContextRbacEntity.builder()
|
||||||
|
.uuid(UUID.randomUUID())
|
||||||
|
.type("SSH")
|
||||||
|
.qualifier("internal")
|
||||||
|
.publicAccess(false)
|
||||||
|
.onlyForNaturalPersons(false)
|
||||||
|
.build()
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+475
@@ -0,0 +1,475 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.accounts;
|
||||||
|
|
||||||
|
import io.restassured.RestAssured;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import lombok.val;
|
||||||
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.hs.accounts.HsCredentialsEntity.HsCredentialsEntityBuilder;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.PersistenceContext;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON;
|
||||||
|
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON;
|
||||||
|
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.not;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class }
|
||||||
|
)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Tag("generalIntegrationTest")
|
||||||
|
// too complex database interaction for just a RestTest, thus a fully integrated test
|
||||||
|
class HsCredentialsControllerAcceptanceTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
|
@LocalServerPort
|
||||||
|
private Integer port;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RbacSubjectRepository subjectRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
HsOfficePersonRealRepository realPersonRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
HsCredentialsContextRealRepository contextRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
|
@PersistenceContext
|
||||||
|
EntityManager em;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class GetCurrentUser {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchCurrentLoginUser() throws Exception {
|
||||||
|
// given
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/hs/accounts/current")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("subject.name", equalTo("superuser-alex@hostsharing.net"))
|
||||||
|
.body("globalAdmin", equalTo(true));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class GetCredentialsByUuid {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFilterInvalidContextsRegardingNonNaturalPerson() {
|
||||||
|
// given
|
||||||
|
val legalPerson = givenLegalPerson("selfregistered-user-drew@hostsharing.org");
|
||||||
|
val credentialsEntity = givenNewCredentials("selfregistered-user-drew@hostsharing.org",
|
||||||
|
"test-subject1", legalPerson, builder -> {
|
||||||
|
builder.loginContexts(new HashSet<>(contextRepo.findAll()));
|
||||||
|
});
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer " + credentialsEntity.getSubject().getName())
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.get("http://localhost/api/hs/accounts/credentials/" + credentialsEntity.getUuid())
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("$", lenientlyEquals("""
|
||||||
|
{
|
||||||
|
"person": {
|
||||||
|
"personType": "LEGAL_PERSON",
|
||||||
|
"tradeName": "Test Company",
|
||||||
|
"salutation": null,
|
||||||
|
"title": null,
|
||||||
|
"givenName": null,
|
||||||
|
"familyName": null
|
||||||
|
},
|
||||||
|
"nickname": "test-subject1",
|
||||||
|
"totpSecrets": null,
|
||||||
|
"phonePassword": null,
|
||||||
|
"emailAddress": null,
|
||||||
|
"smsNumber": null,
|
||||||
|
"active": false,
|
||||||
|
"globalUid": null,
|
||||||
|
"globalGid": null,
|
||||||
|
"onboardingToken": null,
|
||||||
|
"contexts": [
|
||||||
|
{
|
||||||
|
"uuid": "33333333-3333-3333-3333-333333333333",
|
||||||
|
"type": "SSH",
|
||||||
|
"qualifier": "external",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "66666666-6666-6666-6666-666666666666",
|
||||||
|
"type": "MASTODON",
|
||||||
|
"qualifier": "external",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "77777777-7777-7777-7777-777777777777",
|
||||||
|
"type": "BBB",
|
||||||
|
"qualifier": "external",
|
||||||
|
"onlyForNaturalPersons": false,
|
||||||
|
"publicAccess": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"lastUsed": null
|
||||||
|
}
|
||||||
|
"""));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class PostNewCredentials {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCreatingCredentialsForUnrepresentedPerson() {
|
||||||
|
// given
|
||||||
|
val testPerson = givenPersonWithUuid("selfregistered-user-drew@hostsharing.org");
|
||||||
|
val publicContext = contextRepo.findByTypeAndQualifier("SSH", "external").orElseThrow();
|
||||||
|
assertThat(publicContext.isPublicAccess()).as("precondition failed").isTrue();
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer selfregistered-user-drew@hostsharing.org")
|
||||||
|
.header("Accept-Language", "de")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"person.uuid": "%s",
|
||||||
|
"nickname": "new-user",
|
||||||
|
"active": true,
|
||||||
|
"globalUid": 30001,
|
||||||
|
"globalGid": 40001,
|
||||||
|
"contexts": [
|
||||||
|
{
|
||||||
|
"uuid" : "%s"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(testPerson.getUuid(), publicContext.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/hs/accounts/credentials")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("message", containsString("wird von der eingeloggten Person nicht repräsentiert"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCreatingCredentialsWithPrivateContextForNormalUser() {
|
||||||
|
// given
|
||||||
|
val drewPerson = realPersonRepo.findPersonByOptionalNameLike("Drew").getFirst();
|
||||||
|
val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
||||||
|
val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
||||||
|
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer selfregistered-user-drew@hostsharing.org")
|
||||||
|
.header("Accept-Language", "de")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"person.uuid": "%s",
|
||||||
|
"nickname": "new-user",
|
||||||
|
"active": true,
|
||||||
|
"globalUid": 30001,
|
||||||
|
"globalGid": 40001,
|
||||||
|
"contexts": [
|
||||||
|
{ "uuid" : "%s" },
|
||||||
|
{ "uuid" : "%s" },
|
||||||
|
{ "uuid" : "%s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
drewPerson.getUuid(),
|
||||||
|
publicExternalMatrixContext.getUuid(),
|
||||||
|
privateInternalSshContext.getUuid(),
|
||||||
|
privateInternalMatrixContext.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/hs/accounts/credentials")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("message", containsString("Kontext-Zugriff verweigert: 'MATRIX:internal', 'SSH:internal'"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectCreatingCredentialsWithNaturalPersonRequirementForNonNaturalPerson() {
|
||||||
|
// given
|
||||||
|
val firstGmbHPerson = realPersonRepo.findPersonByOptionalNameLike("First").getFirst();
|
||||||
|
val hsadminProdContextOnlyForNaturalPersons = contextRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asNaturalPersonContext).orElseThrow();
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
|
.header("Accept-Language", "de")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"person.uuid": "%s",
|
||||||
|
"nickname": "new-user",
|
||||||
|
"active": true,
|
||||||
|
"globalUid": 30001,
|
||||||
|
"globalGid": 40001,
|
||||||
|
"contexts": [
|
||||||
|
{ "uuid" : "%s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
firstGmbHPerson.getUuid(),
|
||||||
|
hsadminProdContextOnlyForNaturalPersons.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/hs/accounts/credentials")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("message", containsString("Kontext verlangt eine natürliche Person: 'HSADMIN:prod'"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class PatchCredentials {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectPatchingCredentialsWithPrivateContextForNormalUser() {
|
||||||
|
// given
|
||||||
|
context.define("selfregistered-user-drew@hostsharing.org");
|
||||||
|
val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||||
|
.getSubject().getUuid();
|
||||||
|
val privateInternalSshContext = contextRepo.findByTypeAndQualifier("SSH", "internal")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
||||||
|
val privateInternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "internal")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPrivateContext).orElseThrow();
|
||||||
|
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer selfregistered-user-drew@hostsharing.org")
|
||||||
|
.header("Accept-Language", "de")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"contexts": [
|
||||||
|
{ "uuid" : "%s" },
|
||||||
|
{ "uuid" : "%s" },
|
||||||
|
{ "uuid" : "%s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(
|
||||||
|
privateInternalSshContext.getUuid(),
|
||||||
|
publicExternalMatrixContext.getUuid(),
|
||||||
|
privateInternalMatrixContext.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid)
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("message", containsString("Kontext-Zugriff verweigert: 'MATRIX:internal', 'SSH:internal'"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRejectPatchingCredentialsAndRemovingTheOwnHsadminCredentials() {
|
||||||
|
// given
|
||||||
|
context.define("selfregistered-user-drew@hostsharing.org");
|
||||||
|
val drewCredentialsUuid = credentialsRepo.findByCurrentSubject().stream().findFirst().orElseThrow()
|
||||||
|
.getSubject().getUuid();
|
||||||
|
val publicExternalMatrixContext = contextRepo.findByTypeAndQualifier("MATRIX", "external")
|
||||||
|
.map(HsCredentialsControllerAcceptanceTest::asPublicContext).orElseThrow();
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer selfregistered-user-drew@hostsharing.org")
|
||||||
|
.header("Accept-Language", "de")
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body("""
|
||||||
|
{
|
||||||
|
"contexts": [
|
||||||
|
{ "uuid" : "%s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".formatted(publicExternalMatrixContext.getUuid()))
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.patch("http://localhost/api/hs/accounts/credentials/" + drewCredentialsUuid)
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(400)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("message", containsString("die eigenen hsadmin-Credentials dürfen nicht entfernt werden"));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class MarkCredentialsAsUsed {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void markCredentialsAsUsed() {
|
||||||
|
// given
|
||||||
|
val testPerson = givenNaturalPerson("selfregistered-user-drew@hostsharing.org");
|
||||||
|
val credentialsEntity = givenNewCredentials("selfregistered-user-drew@hostsharing.org",
|
||||||
|
"test-subject2",
|
||||||
|
testPerson, builder -> {
|
||||||
|
builder.onboardingToken("some-onboarding-token");
|
||||||
|
builder.loginContexts(contextRepo.findAll().stream()
|
||||||
|
.filter(HsCredentialsContext::isPublicAccess).collect(Collectors.toSet()));
|
||||||
|
});
|
||||||
|
|
||||||
|
RestAssured // @formatter:off
|
||||||
|
.given()
|
||||||
|
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
||||||
|
.port(port)
|
||||||
|
.when()
|
||||||
|
.post("http://localhost/api/hs/accounts/credentials/" + credentialsEntity.getUuid() + "/used")
|
||||||
|
.then().log().all().assertThat()
|
||||||
|
.statusCode(200)
|
||||||
|
.contentType("application/json")
|
||||||
|
.body("uuid", is(credentialsEntity.getUuid().toString()))
|
||||||
|
.body("onboardingToken", is(nullValue()))
|
||||||
|
.body("lastUsed", is(not(nullValue())));
|
||||||
|
// @formatter:on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
private HsOfficePersonRealEntity givenLegalPerson(final String executingSubjectName) {
|
||||||
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define(executingSubjectName);
|
||||||
|
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||||
|
.personType(LEGAL_PERSON)
|
||||||
|
.tradeName("Test Company")
|
||||||
|
.build()));
|
||||||
|
}).assertSuccessful().returnedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsOfficePersonRealEntity givenNaturalPerson(final String executingSubjectName) {
|
||||||
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define(executingSubjectName);
|
||||||
|
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||||
|
.personType(NATURAL_PERSON)
|
||||||
|
.familyName("Test")
|
||||||
|
.givenName("User")
|
||||||
|
.build()));
|
||||||
|
}).assertSuccessful().returnedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsOfficePersonRealEntity givenPersonWithUuid(final String executingSubjectName) {
|
||||||
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define(executingSubjectName);
|
||||||
|
return toCleanup(realPersonRepo.save(HsOfficePersonRealEntity.builder()
|
||||||
|
.personType(NATURAL_PERSON)
|
||||||
|
.familyName("Test")
|
||||||
|
.givenName("Person")
|
||||||
|
.build()));
|
||||||
|
}).returnedValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HsCredentialsContextRealEntity asNaturalPersonContext(@NotNull HsCredentialsContextRealEntity context) {
|
||||||
|
assertThat(context.isOnlyForNaturalPersons()).as("precondition failed").isTrue();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HsCredentialsContextRealEntity asPrivateContext(@NotNull HsCredentialsContextRealEntity context) {
|
||||||
|
assertThat(context.isPublicAccess()).as("precondition failed").isFalse();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HsCredentialsContextRealEntity asPublicContext(@NotNull HsCredentialsContextRealEntity context) {
|
||||||
|
assertThat(context.isPublicAccess()).as("precondition failed").isTrue();
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HsCredentialsEntity givenNewCredentials(
|
||||||
|
final String executingSubjectName,
|
||||||
|
final String newSubjectName, final HsOfficePersonRealEntity person,
|
||||||
|
final Consumer<HsCredentialsEntityBuilder> modifier
|
||||||
|
) {
|
||||||
|
return jpaAttempt.transacted(() -> {
|
||||||
|
context.define(executingSubjectName);
|
||||||
|
final RbacSubjectEntity rbacSubjectEntity = RbacSubjectEntity.builder()
|
||||||
|
.name(newSubjectName)
|
||||||
|
.build();
|
||||||
|
val subject = subjectRepo.create(rbacSubjectEntity);
|
||||||
|
|
||||||
|
context.define(subject.getName());
|
||||||
|
val attachedPerson = em.find(HsOfficePersonRealEntity.class, person.getUuid());
|
||||||
|
val credentialsBuilder = HsCredentialsEntity.builder()
|
||||||
|
.person(attachedPerson)
|
||||||
|
.subject(subjectRepo.findByUuid(subject.getUuid()))
|
||||||
|
.loginContexts(Set.of());
|
||||||
|
modifier.accept(credentialsBuilder);
|
||||||
|
return toCleanup(credentialsRepo.save(credentialsBuilder.build()));
|
||||||
|
}).assertSuccessful().returnedValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
-314
@@ -1,314 +0,0 @@
|
|||||||
package net.hostsharing.hsadminng.hs.accounts;
|
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
|
||||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
|
||||||
import net.hostsharing.hsadminng.config.MessagesResourceConfig;
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
|
||||||
import org.hamcrest.CustomMatcher;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
|
||||||
import org.springframework.context.annotation.Import;
|
|
||||||
import org.springframework.http.MediaType;
|
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
|
||||||
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
|
||||||
import org.springframework.test.web.servlet.MockMvc;
|
|
||||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
|
|
||||||
|
|
||||||
import jakarta.persistence.EntityManagerFactory;
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.LEGAL_PERSON;
|
|
||||||
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON;
|
|
||||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
|
||||||
import static org.hamcrest.Matchers.containsString;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
|
||||||
import static org.mockito.BDDMockito.given;
|
|
||||||
import static org.mockito.Mockito.when;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
|
||||||
|
|
||||||
@WebMvcTest(HsCredentialsController.class)
|
|
||||||
@Import({
|
|
||||||
StrictMapper.class,
|
|
||||||
JsonObjectMapperConfiguration.class,
|
|
||||||
DisableSecurityConfig.class,
|
|
||||||
// HOWTO: test i18n translations
|
|
||||||
MessagesResourceConfig.class,
|
|
||||||
MessageTranslator.class })
|
|
||||||
@ActiveProfiles("test")
|
|
||||||
class HsCredentialsControllerRestTest {
|
|
||||||
|
|
||||||
private static final UUID PERSON_UUID = UUID.randomUUID();
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
MockMvc mockMvc;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
Context contextMock;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@SuppressWarnings("unused") // not used in test, but in controller class
|
|
||||||
StrictMapper mapper;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
EntityManagerWrapper em;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
EntityManagerFactory emf;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
RbacSubjectRepository subjectRepo;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
HsOfficePersonRealRepository realPersonRepo;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
HsOfficePersonRbacRepository rbacPersonRepo;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
HsCredentialsRepository credentialsRepo;
|
|
||||||
|
|
||||||
@MockitoBean
|
|
||||||
CredentialContextResourceToEntityMapper contextMapper;
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFetchCurrentLoginUser() throws Exception {
|
|
||||||
// given
|
|
||||||
final UUID currentSubjectUuid = UUID.randomUUID();
|
|
||||||
given(contextMock.fetchCurrentSubjectUuid()).willReturn(currentSubjectUuid);
|
|
||||||
given(contextMock.isGlobalAdmin()).willReturn(true);
|
|
||||||
given(subjectRepo.findByUuid(currentSubjectUuid)).willReturn(
|
|
||||||
RbacSubjectEntity.builder().uuid(currentSubjectUuid).name("test-user").build()
|
|
||||||
);
|
|
||||||
given(credentialsRepo.findByUuid(currentSubjectUuid)).willReturn(
|
|
||||||
Optional.of(HsCredentialsEntity.builder()
|
|
||||||
.uuid(currentSubjectUuid)
|
|
||||||
.person(HsOfficePersonRbacEntity.builder()
|
|
||||||
.uuid(PERSON_UUID)
|
|
||||||
.personType(NATURAL_PERSON)
|
|
||||||
.familyName("Miller")
|
|
||||||
.givenName("Steph")
|
|
||||||
.build())
|
|
||||||
.subject(RbacSubjectEntity.builder().name("steph-miller").build())
|
|
||||||
.build())
|
|
||||||
);
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.get("/api/hs/accounts/current")
|
|
||||||
.header("Authorization", "Bearer test")
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
.andDo(print())
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.subject.uuid").value(currentSubjectUuid.toString()))
|
|
||||||
.andExpect(jsonPath("$.subject.name").value("test-user"))
|
|
||||||
.andExpect(jsonPath("$.person.uuid").value(PERSON_UUID.toString()))
|
|
||||||
.andExpect(jsonPath("$.person.familyName").value("Miller"))
|
|
||||||
.andExpect(jsonPath("$.person.givenName").value("Steph"))
|
|
||||||
.andExpect(jsonPath("$.globalAdmin").value(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldFilterInvalidContextsRegardingNonNaturalPerson() throws Exception {
|
|
||||||
// given
|
|
||||||
final var givenCredentialsUuid = UUID.randomUUID();
|
|
||||||
final var contextForNP = HsCredentialsContextRealEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
|
||||||
.type("HSADMIN")
|
|
||||||
.qualifier("prod")
|
|
||||||
.onlyForNaturalPersons(true)
|
|
||||||
.build();
|
|
||||||
final var contextForAll = HsCredentialsContextRealEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
|
||||||
.type("SSH")
|
|
||||||
.qualifier("prod")
|
|
||||||
.onlyForNaturalPersons(false)
|
|
||||||
.build();
|
|
||||||
final var credentialsEntity = HsCredentialsEntity.builder()
|
|
||||||
.uuid(givenCredentialsUuid)
|
|
||||||
.person(HsOfficePersonRbacEntity.builder()
|
|
||||||
.uuid(PERSON_UUID)
|
|
||||||
.personType(LEGAL_PERSON)
|
|
||||||
.build())
|
|
||||||
.subject(RbacSubjectEntity.builder().name("some-nickname").build())
|
|
||||||
.loginContexts(Set.of(contextForNP, contextForAll))
|
|
||||||
.build();
|
|
||||||
when(credentialsRepo.findByUuid(givenCredentialsUuid))
|
|
||||||
.thenReturn(Optional.of(credentialsEntity));
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.get("/api/hs/accounts/credentials/" + givenCredentialsUuid)
|
|
||||||
.header("Authorization", "Bearer test")
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
.andDo(print())
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath("$.contexts.length()").value(1))
|
|
||||||
.andExpect(jsonPath("$.contexts[0].type").value("SSH"))
|
|
||||||
.andExpect(jsonPath("$.contexts[0].qualifier").value("prod"))
|
|
||||||
.andExpect(jsonPath("$.contexts[0].onlyForNaturalPersons").value(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectCreatingCredentialsForUnrepresentedPerson() throws Exception {
|
|
||||||
// given
|
|
||||||
final var personUuid = UUID.randomUUID();
|
|
||||||
|
|
||||||
final AtomicReference<RbacSubjectEntity> createdSubject = new AtomicReference<>();
|
|
||||||
given(subjectRepo.create(any())).willAnswer(invocation -> {
|
|
||||||
final var passedEntity = (RbacSubjectEntity) invocation.getArgument(0);
|
|
||||||
passedEntity.setUuid(UUID.randomUUID());
|
|
||||||
createdSubject.set(passedEntity); // Capture the instance
|
|
||||||
return passedEntity;
|
|
||||||
});
|
|
||||||
given(contextMock.fetchCurrentSubject()).willAnswer(invocation -> createdSubject.get().getName());
|
|
||||||
given(subjectRepo.findByUuid(any())).willAnswer(invocation -> createdSubject.get());
|
|
||||||
given(rbacPersonRepo.findByUuid(personUuid)).willReturn(Optional.of(
|
|
||||||
HsOfficePersonRbacEntity.builder().uuid(personUuid).personType(NATURAL_PERSON).build()
|
|
||||||
));
|
|
||||||
given(rbacPersonRepo.findPersonsRepresentedByPersonWithUuid(personUuid)).willReturn(List.of(
|
|
||||||
// some persons, but not the one from the login-user itself
|
|
||||||
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(NATURAL_PERSON).build(),
|
|
||||||
HsOfficePersonRbacEntity.builder().uuid(UUID.randomUUID()).personType(LEGAL_PERSON).build()
|
|
||||||
));
|
|
||||||
|
|
||||||
final var givenCredentialsUuid = UUID.randomUUID();
|
|
||||||
|
|
||||||
final var contextForNP = HsCredentialsContextRealEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
|
||||||
.type("HSADMIN")
|
|
||||||
.qualifier("prod")
|
|
||||||
.onlyForNaturalPersons(true)
|
|
||||||
.build();
|
|
||||||
final var contextForAll = HsCredentialsContextRealEntity.builder()
|
|
||||||
.uuid(UUID.randomUUID())
|
|
||||||
.type("SSH")
|
|
||||||
.qualifier("prod")
|
|
||||||
.onlyForNaturalPersons(false)
|
|
||||||
.build();
|
|
||||||
final var credentialsEntity = HsCredentialsEntity.builder()
|
|
||||||
.uuid(givenCredentialsUuid)
|
|
||||||
.person(HsOfficePersonRbacEntity.builder()
|
|
||||||
.uuid(PERSON_UUID)
|
|
||||||
.personType(LEGAL_PERSON)
|
|
||||||
.build())
|
|
||||||
.subject(RbacSubjectEntity.builder().name("some-nickname").build())
|
|
||||||
.loginContexts(Set.of(contextForNP, contextForAll))
|
|
||||||
.build();
|
|
||||||
when(credentialsRepo.findByUuid(givenCredentialsUuid))
|
|
||||||
.thenReturn(Optional.of(credentialsEntity));
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.post("/api/hs/accounts/credentials")
|
|
||||||
.header("Authorization", "Bearer test")
|
|
||||||
// HOWTO: test i18n translations
|
|
||||||
.header("Accept-Language", "de")
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.content(
|
|
||||||
"""
|
|
||||||
{
|
|
||||||
"person.uuid": "${personUuid}",
|
|
||||||
"nickname": "${nickname}",
|
|
||||||
"active": true,
|
|
||||||
"globalUid": 30001,
|
|
||||||
"globalGid": 40001,
|
|
||||||
"contexts": [
|
|
||||||
{
|
|
||||||
"uuid" : "11111111-1111-1111-1111-111111111111",
|
|
||||||
"type" : "HSADMIN",
|
|
||||||
"qualifier" : "prod",
|
|
||||||
"onlyForNaturalPersons" : true,
|
|
||||||
"publicAccess" : true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
.replace("${personUuid}", personUuid.toString())
|
|
||||||
.replace("${nickname}", "new-user")
|
|
||||||
)
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
.andDo(print())
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().is4xxClientError())
|
|
||||||
.andExpect(jsonPath("$.message", containsString(
|
|
||||||
"Zugriff verweigert: personUuid \"${personUuid}\" wird von der eingeloggten Person nicht repräsentiert"
|
|
||||||
.replace("${personUuid}", personUuid.toString()))));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void markCredentialsAsUsed() throws Exception {
|
|
||||||
|
|
||||||
// given
|
|
||||||
final var givenCredentialsUuid = UUID.randomUUID();
|
|
||||||
when(credentialsRepo.findByUuid(givenCredentialsUuid)).thenReturn(Optional.of(
|
|
||||||
HsCredentialsEntity.builder()
|
|
||||||
.uuid(givenCredentialsUuid)
|
|
||||||
.person(HsOfficePersonRbacEntity.builder().uuid(PERSON_UUID).build())
|
|
||||||
.subject(RbacSubjectEntity.builder().name("some-nickname").build())
|
|
||||||
.lastUsed(null)
|
|
||||||
.onboardingToken("fake-onboarding-token")
|
|
||||||
.build()
|
|
||||||
));
|
|
||||||
when(credentialsRepo.save(any())).thenAnswer(invocation ->
|
|
||||||
invocation.getArgument(0)
|
|
||||||
);
|
|
||||||
|
|
||||||
// when
|
|
||||||
mockMvc.perform(MockMvcRequestBuilders
|
|
||||||
.post("/api/hs/accounts/credentials/%{credentialsUuid}/used"
|
|
||||||
.replace("%{credentialsUuid}", givenCredentialsUuid.toString()))
|
|
||||||
.header("Authorization", "Bearer superuser-alex@hostsharing.net")
|
|
||||||
.accept(MediaType.APPLICATION_JSON))
|
|
||||||
.andDo(print())
|
|
||||||
|
|
||||||
// then
|
|
||||||
.andExpect(status().isOk())
|
|
||||||
.andExpect(jsonPath(
|
|
||||||
"$", lenientlyEquals("""
|
|
||||||
{
|
|
||||||
"uuid": "%{credentialsUuid}",
|
|
||||||
"onboardingToken": null
|
|
||||||
}
|
|
||||||
""".replace("%{credentialsUuid}", givenCredentialsUuid.toString())
|
|
||||||
)))
|
|
||||||
.andExpect(jsonPath("$.lastUsed").value(new CustomMatcher<String>("lastUsed should have recent timestamp") {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean matches(final Object o) {
|
|
||||||
if (o == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
final var lastUsed = ZonedDateTime.parse(o.toString(), DateTimeFormatter.ISO_DATE_TIME)
|
|
||||||
.toLocalDateTime();
|
|
||||||
return lastUsed.isAfter(LocalDateTime.now().minusMinutes(1)) &&
|
|
||||||
lastUsed.isBefore(LocalDateTime.now());
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+10
-10
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
import net.hostsharing.hsadminng.hs.office.contact.HsOfficeContactRealEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||||
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.hs.office.relation.HsOfficeRelationRealEntity;
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
@@ -62,8 +62,8 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
private RbacSubjectEntity alexSubject;
|
private RbacSubjectEntity alexSubject;
|
||||||
private RbacSubjectEntity drewSubject;
|
private RbacSubjectEntity drewSubject;
|
||||||
private RbacSubjectEntity testUserSubject;
|
private RbacSubjectEntity testUserSubject;
|
||||||
private HsOfficePersonRbacEntity drewPerson;
|
private HsOfficePersonRealEntity drewPerson;
|
||||||
private HsOfficePersonRbacEntity testUserPerson;
|
private HsOfficePersonRealEntity testUserPerson;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@@ -277,13 +277,13 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HsOfficePersonRbacEntity fetchPersonByGivenName(final String givenName) {
|
private HsOfficePersonRealEntity fetchPersonByGivenName(final String givenName) {
|
||||||
final String jpql = "SELECT p FROM HsOfficePersonRbacEntity p WHERE p.givenName = :givenName";
|
final String jpql = "SELECT p FROM HsOfficePersonRealEntity p WHERE p.givenName = :givenName";
|
||||||
final Query query = em.createQuery(jpql, HsOfficePersonRbacEntity.class);
|
final Query query = em.createQuery(jpql, HsOfficePersonRealEntity.class);
|
||||||
query.setParameter("givenName", givenName);
|
query.setParameter("givenName", givenName);
|
||||||
try {
|
try {
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
return notNull((HsOfficePersonRbacEntity) query.getSingleResult());
|
return notNull((HsOfficePersonRealEntity) query.getSingleResult());
|
||||||
} catch (final NoResultException e) {
|
} catch (final NoResultException e) {
|
||||||
throw new AssertionError(
|
throw new AssertionError(
|
||||||
"Failed to find person with name '" + givenName + "'. Ensure test data is present.", e);
|
"Failed to find person with name '" + givenName + "'. Ensure test data is present.", e);
|
||||||
@@ -315,7 +315,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
private class RelationBuilder {
|
private class RelationBuilder {
|
||||||
private final HsOfficeRelationType relationType;
|
private final HsOfficeRelationType relationType;
|
||||||
private HsOfficePersonRealEntity anchorPerson;
|
private HsOfficePersonRealEntity anchorPerson;
|
||||||
private HsOfficePersonRbacEntity holderPerson;
|
private HsOfficePersonRealEntity holderPerson;
|
||||||
private HsOfficeContactRealEntity contact;
|
private HsOfficeContactRealEntity contact;
|
||||||
|
|
||||||
public RelationBuilder(HsOfficeRelationType relationType) {
|
public RelationBuilder(HsOfficeRelationType relationType) {
|
||||||
@@ -327,7 +327,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RelationBuilder withHolder(HsOfficePersonRbacEntity holderPerson) {
|
public RelationBuilder withHolder(HsOfficePersonRealEntity holderPerson) {
|
||||||
this.holderPerson = holderPerson;
|
this.holderPerson = holderPerson;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -373,7 +373,7 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup
|
|||||||
final var credentials = HsCredentialsEntity.builder()
|
final var credentials = HsCredentialsEntity.builder()
|
||||||
.uuid(subject.getUuid())
|
.uuid(subject.getUuid())
|
||||||
.subject(subject)
|
.subject(subject)
|
||||||
.person(em.find(HsOfficePersonRbacEntity.class, person.getUuid()))
|
.person(em.find(HsOfficePersonRealEntity.class, person.getUuid()))
|
||||||
.emailAddress(emailAddress)
|
.emailAddress(emailAddress)
|
||||||
.active(true)
|
.active(true)
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
+34
-1
@@ -150,6 +150,39 @@ class HsOfficePersonRealRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findPersonsRepresentedByPersonWithUuid() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
final var personUuid = personRealRepo.findPersonByOptionalNameLike("Fouler").getFirst().getUuid();
|
||||||
|
|
||||||
|
// when
|
||||||
|
@SuppressWarnings("unchecked") final List<HsOfficePersonRealEntity> representedPersons = personRealRepo.findPersonsRepresentedByPersonWithUuid(personUuid);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder(
|
||||||
|
"person(personType=NP, familyName='Fouler', givenName='Ellie')",
|
||||||
|
"person(personType=LP, tradeName='Fourth eG')"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findPersonsRepresentedByPersonWithUuidDrew() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
final var personUuid = personRealRepo.findPersonByOptionalNameLike("Drew").getFirst().getUuid();
|
||||||
|
|
||||||
|
// when
|
||||||
|
@SuppressWarnings("unchecked") final List<HsOfficePersonRealEntity> representedPersons = personRealRepo.findPersonsRepresentedByPersonWithUuid(personUuid);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder(
|
||||||
|
"person(personType=NP, familyName='User', givenName='Drew')"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void auditJournalLogIsAvailable() {
|
public void auditJournalLogIsAvailable() {
|
||||||
// given
|
// given
|
||||||
@@ -157,7 +190,7 @@ class HsOfficePersonRealRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
select currentTask, targetTable, targetOp, targetdelta->>'tradename', targetdelta->>'lastname'
|
select currentTask, targetTable, targetOp, targetdelta->>'tradename', targetdelta->>'lastname'
|
||||||
from base.tx_journal_v
|
from base.tx_journal_v
|
||||||
where targettable = 'hs_office.person';
|
where targettable = 'hs_office.person';
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
@SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
|
@SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import java.util.Optional;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
|
||||||
public class EntityManagerWrapperFake extends EntityManagerWrapper {
|
public class EntityManagerWrapperFake extends EntityManagerWrapper {
|
||||||
|
|
||||||
private Map<Class<?>, Map<Object, Object>> entityClasses = new HashMap<>();
|
private Map<Class<?>, Map<Object, Object>> entityClasses = new HashMap<>();
|
||||||
|
|||||||
+9
@@ -5,6 +5,7 @@ import net.hostsharing.hsadminng.context.Context;
|
|||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||||
@@ -19,7 +20,9 @@ import java.util.UUID;
|
|||||||
|
|
||||||
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
|
import static net.hostsharing.hsadminng.rbac.test.IsValidUuidMatcher.isUuidValid;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
@@ -42,6 +45,12 @@ class RbacSubjectControllerRestTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
EntityManagerWrapper em;
|
EntityManagerWrapper em;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void beforeEach() {
|
||||||
|
given(rbacSubjectRepository.create(any())).willAnswer(invocation ->
|
||||||
|
invocation.<RbacSubjectEntity>getArgument(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void postNewSubjectUsesGivenUuid() throws Exception {
|
void postNewSubjectUsesGivenUuid() throws Exception {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
JpaAttempt jpaAttempt;
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
private TreeMap<UUID, Class<? extends ImmutableBaseEntity>> entitiesToCleanup = new TreeMap<>();
|
private LinkedHashMap<UUID, Class<? extends ImmutableBaseEntity>> entitiesToCleanup = new LinkedHashMap<>();
|
||||||
|
|
||||||
private static Long latestIntialTestDataSerialId;
|
private static Long latestIntialTestDataSerialId;
|
||||||
private static boolean countersInitialized = false;
|
private static boolean countersInitialized = false;
|
||||||
@@ -102,6 +102,10 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
? tableName.substring(0, tableName.length() - "_rv".length())
|
? tableName.substring(0, tableName.length() - "_rv".length())
|
||||||
: tableName;
|
: tableName;
|
||||||
|
|
||||||
|
final var rawTableName = rvTableName.endsWith("_rv")
|
||||||
|
? rvTableName.substring(0, rvTableName.length() - "_rv".length())
|
||||||
|
: rvTableName;
|
||||||
|
|
||||||
allRbacObjects().stream()
|
allRbacObjects().stream()
|
||||||
.filter(o -> o.startsWith(rvTableName + ":"))
|
.filter(o -> o.startsWith(rvTableName + ":"))
|
||||||
.filter(o -> !initialRbacObjects.contains(o))
|
.filter(o -> !initialRbacObjects.contains(o))
|
||||||
@@ -191,11 +195,11 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
context.define("superuser-alex@hostsharing.net", null);
|
context.define("superuser-alex@hostsharing.net", null);
|
||||||
entitiesToCleanup.reversed().forEach((uuid, entityClass) -> {
|
entitiesToCleanup.reversed().forEach((uuid, entityClass) -> {
|
||||||
final var rvTableName = entityClass.getAnnotation(Table.class).name();
|
final var rvTableName = entityClass.getAnnotation(Table.class).name();
|
||||||
if ( !rvTableName.endsWith("_rv") ) {
|
final var scope = entityClass.getAnnotation(Table.class).schema();
|
||||||
throw new IllegalStateException();
|
final var rawTableName = rvTableName.endsWith("_rv")
|
||||||
}
|
? rvTableName.substring(0, rvTableName.length() - "_rv".length())
|
||||||
final var rawTableName = rvTableName.substring(0, rvTableName.length() - "_rv".length());
|
: rvTableName;
|
||||||
final var deletedRows = em.createNativeQuery("DELETE FROM " + rawTableName + " WHERE uuid=:uuid")
|
final var deletedRows = em.createNativeQuery("DELETE FROM " + scope + "." + rawTableName + " WHERE uuid=:uuid")
|
||||||
.setParameter("uuid", uuid).executeUpdate();
|
.setParameter("uuid", uuid).executeUpdate();
|
||||||
out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " deleted " + deletedRows + " rows");
|
out.println("DELETING temporary " + entityClass.getSimpleName() + "#" + uuid + " deleted " + deletedRows + " rows");
|
||||||
});
|
});
|
||||||
@@ -264,6 +268,9 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
assertThat(after).isNotNull();
|
assertThat(after).isNotNull();
|
||||||
final SetUtils.SetView<String> difference = difference(before, after);
|
final SetUtils.SetView<String> difference = difference(before, after);
|
||||||
assertThat(difference).as("missing entities (deleted initial test data)").isEmpty();
|
assertThat(difference).as("missing entities (deleted initial test data)").isEmpty();
|
||||||
|
difference(after, before).stream().iterator().forEachRemaining(e -> {
|
||||||
|
em.remove(e);
|
||||||
|
});
|
||||||
assertThat(difference(after, before)).as("spurious entities (test data not cleaned up by this test)").isEmpty();
|
assertThat(difference(after, before)).as("spurious entities (test data not cleaned up by this test)").isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user