bugfix: fixes HTTP POST on credentials, including person+subject (#184)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/184 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityManager;
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CredentialContextResourceToEntityMapper {
|
||||||
|
|
||||||
|
private final EntityManager em;
|
||||||
|
private final MessageTranslator messageTranslator;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public CredentialContextResourceToEntityMapper(EntityManager em, MessageTranslator messageTranslator) {
|
||||||
|
this.em = em;
|
||||||
|
this.messageTranslator = messageTranslator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<HsCredentialsContextRealEntity> mapCredentialsToContextEntities(
|
||||||
|
List<ContextResource> resources
|
||||||
|
) {
|
||||||
|
final var entities = new HashSet<HsCredentialsContextRealEntity>();
|
||||||
|
syncCredentialsContextEntities(resources, entities);
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncCredentialsContextEntities(
|
||||||
|
List<ContextResource> resources,
|
||||||
|
Set<HsCredentialsContextRealEntity> entities
|
||||||
|
) {
|
||||||
|
final var resourceUuids = resources.stream()
|
||||||
|
.map(ContextResource::getUuid)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
final var entityUuids = entities.stream()
|
||||||
|
.map(HsCredentialsContextRealEntity::getUuid)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
|
entities.removeIf(e -> !resourceUuids.contains(e.getUuid()));
|
||||||
|
|
||||||
|
for (final var resource : resources) {
|
||||||
|
if (!entityUuids.contains(resource.getUuid())) {
|
||||||
|
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
||||||
|
if (existingContextEntity == null) {
|
||||||
|
throw new EntityNotFoundException(
|
||||||
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
||||||
|
"credentials uuid", resource.getUuid()));
|
||||||
|
}
|
||||||
|
if (!existingContextEntity.getType().equals(resource.getType()) &&
|
||||||
|
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
||||||
|
throw new EntityNotFoundException(
|
||||||
|
messageTranslator.translate("existing {0} does not match given resource {1}",
|
||||||
|
existingContextEntity, resource));
|
||||||
|
}
|
||||||
|
entities.add(existingContextEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,7 @@ import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResou
|
|||||||
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;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@@ -26,6 +27,7 @@ public class HsCredentialsContextsController implements ContextsApi {
|
|||||||
private HsCredentialsContextRbacRepository contextRepo;
|
private HsCredentialsContextRbacRepository contextRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.contexts.getListOfLoginContexts")
|
@Timed("app.credentials.contexts.getListOfLoginContexts")
|
||||||
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
|
public ResponseEntity<List<ContextResource>> getListOfContexts(final String assumedRoles) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
@@ -2,7 +2,6 @@ package net.hostsharing.hsadminng.credentials;
|
|||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
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;
|
||||||
@@ -15,21 +14,27 @@ import net.hostsharing.hsadminng.credentials.generated.api.v1.api.CredentialsApi
|
|||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsInsertResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsInsertResource;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsResource;
|
||||||
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.HsOfficePersonResource;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
|
import static java.util.Optional.ofNullable;
|
||||||
|
import static java.util.Optional.of;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@SecurityRequirement(name = "casTicket")
|
@SecurityRequirement(name = "casTicket")
|
||||||
public class HsCredentialsController implements CredentialsApi {
|
public class HsCredentialsController implements CredentialsApi {
|
||||||
|
|
||||||
private static DateTimeFormatter FULL_TIMESTAMP_FORMAT = DateTimeFormatter.BASIC_ISO_DATE;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private Context context;
|
private Context context;
|
||||||
|
|
||||||
@@ -39,16 +44,23 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StrictMapper mapper;
|
private StrictMapper mapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RbacSubjectRepository subjectRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private CredentialContextResourceToEntityMapper contextMapper;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private MessageTranslator messageTranslator;
|
private MessageTranslator messageTranslator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsOfficePersonRbacRepository personRepo;
|
private HsOfficePersonRbacRepository rbacPersonRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepo;
|
private HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
||||||
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(
|
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@@ -56,11 +68,16 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var credentials = credentialsRepo.findByUuid(credentialsUuid);
|
final var credentials = credentialsRepo.findByUuid(credentialsUuid);
|
||||||
final var result = mapper.map(credentials, CredentialsResource.class);
|
if (credentials.isEmpty()) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
final var result = mapper.map(
|
||||||
|
credentials.get(), CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
|
@Timed("app.credentials.credentials.getListOfCredentialsByPersonUuid")
|
||||||
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid(
|
public ResponseEntity<List<CredentialsResource>> getListOfCredentialsByPersonUuid(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@@ -68,18 +85,20 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var person = personRepo.findByUuid(personUuid).orElseThrow(
|
final var person = rbacPersonRepo.findByUuid(personUuid).orElseThrow(
|
||||||
() -> new EntityNotFoundException(
|
() -> new EntityNotFoundException(
|
||||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", personUuid)
|
||||||
)
|
)
|
||||||
|
|
||||||
);
|
);
|
||||||
final var credentials = credentialsRepo.findByPerson(person);
|
final var credentials = credentialsRepo.findByPerson(person);
|
||||||
final var result = mapper.mapList(credentials, CredentialsResource.class);
|
final var result = mapper.mapList(
|
||||||
|
credentials, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(result);
|
return ResponseEntity.ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.postNewCredentials")
|
@Timed("app.credentials.credentials.postNewCredentials")
|
||||||
public ResponseEntity<CredentialsResource> postNewCredentials(
|
public ResponseEntity<CredentialsResource> postNewCredentials(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@@ -87,13 +106,28 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var newCredentialsEntity = mapper.map(body, HsCredentialsEntity.class);
|
// first create and save the subject to get its UUID
|
||||||
final var savedCredentialsEntity = credentialsRepo.save(newCredentialsEntity);
|
final var newlySavedSubject = createSubject(body.getNickname());
|
||||||
final var newCredentialsResource = mapper.map(savedCredentialsEntity, CredentialsResource.class);
|
|
||||||
return ResponseEntity.ok(newCredentialsResource);
|
// afterward, create and save the credentials entity with the subject's UUID
|
||||||
|
final var newCredentialsEntity = mapper.map(
|
||||||
|
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
|
newCredentialsEntity.setSubject(newlySavedSubject);
|
||||||
|
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
||||||
|
|
||||||
|
// return the new credentials as a resource
|
||||||
|
final var uri =
|
||||||
|
MvcUriComponentsBuilder.fromController(getClass())
|
||||||
|
.path("/api/hs/credentials/credentials/{id}")
|
||||||
|
.buildAndExpand(newCredentialsEntity.getUuid())
|
||||||
|
.toUri();
|
||||||
|
final var newCredentialsResource = mapper.map(
|
||||||
|
newCredentialsEntity, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
|
return ResponseEntity.created(uri).body(newCredentialsResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
||||||
public ResponseEntity<Void> deleteCredentialsByUuid(final String assumedRoles, final UUID credentialsUuid) {
|
public ResponseEntity<Void> deleteCredentialsByUuid(final String assumedRoles, final UUID credentialsUuid) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
@@ -103,6 +137,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.patchCredentials")
|
@Timed("app.credentials.credentials.patchCredentials")
|
||||||
public ResponseEntity<CredentialsResource> patchCredentials(
|
public ResponseEntity<CredentialsResource> patchCredentials(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
@@ -113,10 +148,11 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
|
|
||||||
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
new HsCredentialsEntityPatcher(em, messageTranslator, current).apply(body);
|
new HsCredentialsEntityPatcher(contextMapper, current).apply(body);
|
||||||
|
|
||||||
final var saved = credentialsRepo.save(current);
|
final var saved = credentialsRepo.save(current);
|
||||||
final var mapped = mapper.map(saved, CredentialsResource.class);
|
final var mapped = mapper.map(
|
||||||
|
saved, CredentialsResource.class, ENTITY_TO_RESOURCE_POSTMAPPER);
|
||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +173,38 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RbacSubjectEntity createSubject(final String nickname) {
|
||||||
|
final var newRbacSubject = subjectRepo.create(new RbacSubjectEntity(null, nickname));
|
||||||
|
if(context.fetchCurrentSubject() == null) {
|
||||||
|
context.define("activate newly created self-servie subject", null, nickname, null);
|
||||||
|
}
|
||||||
|
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // attached to EM
|
||||||
|
}
|
||||||
|
|
||||||
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
resource.setLastUsed(entity.getLastUsed().atOffset(ZoneOffset.UTC));
|
ofNullable(entity.getLastUsed()).ifPresent(
|
||||||
|
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
|
||||||
|
of(entity.getSubject()).ifPresent(
|
||||||
|
subject -> resource.setNickname(subject.getName())
|
||||||
|
);
|
||||||
|
of(entity.getPerson()).ifPresent(
|
||||||
|
person -> resource.setPerson(
|
||||||
|
mapper.map(person, HsOfficePersonResource.class)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
final BiConsumer<CredentialsInsertResource, HsCredentialsEntity> RESOURCE_TO_ENTITY_POSTMAPPER = (resource, entity) -> {
|
||||||
|
|
||||||
|
// TODO.impl: we need to make sure that the current subject is OWNER (or ADMIN?) of the person
|
||||||
|
final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
||||||
|
() -> new EntityNotFoundException(
|
||||||
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
entity.setLoginContexts(contextMapper.mapCredentialsToContextEntities(resource.getContexts()));
|
||||||
|
|
||||||
|
entity.setPerson(person);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@ package net.hostsharing.hsadminng.credentials;
|
|||||||
|
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
||||||
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;
|
||||||
@@ -45,7 +45,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 HsOfficePersonRealEntity person;
|
private HsOfficePersonRbacEntity person;
|
||||||
|
|
||||||
@Version
|
@Version
|
||||||
private int version;
|
private int version;
|
||||||
@@ -92,6 +92,11 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
return loginContexts;
|
return loginContexts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSubject(final RbacSubjectEntity subject) {
|
||||||
|
this.uuid = subject.getUuid();
|
||||||
|
this.subject = subject;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toShortString() {
|
public String toShortString() {
|
||||||
return active + ":" + emailAddress + ":" + globalUid;
|
return active + ":" + emailAddress + ":" + globalUid;
|
||||||
|
@@ -1,26 +1,17 @@
|
|||||||
package net.hostsharing.hsadminng.credentials;
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.ContextResource;
|
|
||||||
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
import net.hostsharing.hsadminng.credentials.generated.api.v1.model.CredentialsPatchResource;
|
||||||
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 jakarta.persistence.EntityManager;
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> {
|
public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatchResource> {
|
||||||
|
|
||||||
private final EntityManager em;
|
private CredentialContextResourceToEntityMapper contextMapper;
|
||||||
private MessageTranslator messageTranslator;
|
|
||||||
private final HsCredentialsEntity entity;
|
private final HsCredentialsEntity entity;
|
||||||
|
|
||||||
public HsCredentialsEntityPatcher(final EntityManager em, MessageTranslator messageTranslator, final HsCredentialsEntity entity) {
|
public HsCredentialsEntityPatcher(final CredentialContextResourceToEntityMapper contextMapper, final HsCredentialsEntity entity) {
|
||||||
this.em = em;
|
this.contextMapper = contextMapper;
|
||||||
this.messageTranslator = messageTranslator;
|
|
||||||
this.entity = entity;
|
this.entity = entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,40 +29,7 @@ public class HsCredentialsEntityPatcher implements EntityPatcher<CredentialsPatc
|
|||||||
OptionalFromJson.of(resource.getPhonePassword())
|
OptionalFromJson.of(resource.getPhonePassword())
|
||||||
.ifPresent(entity::setPhonePassword);
|
.ifPresent(entity::setPhonePassword);
|
||||||
if (resource.getContexts() != null) {
|
if (resource.getContexts() != null) {
|
||||||
syncLoginContextEntities(resource.getContexts(), entity.getLoginContexts());
|
contextMapper.syncCredentialsContextEntities(resource.getContexts(), entity.getLoginContexts());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void syncLoginContextEntities(
|
|
||||||
List<ContextResource> resources,
|
|
||||||
Set<HsCredentialsContextRealEntity> entities
|
|
||||||
) {
|
|
||||||
final var resourceUuids = resources.stream()
|
|
||||||
.map(ContextResource::getUuid)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
final var entityUuids = entities.stream()
|
|
||||||
.map(HsCredentialsContextRealEntity::getUuid)
|
|
||||||
.collect(Collectors.toSet());
|
|
||||||
|
|
||||||
entities.removeIf(e -> !resourceUuids.contains(e.getUuid()));
|
|
||||||
|
|
||||||
for (final var resource : resources) {
|
|
||||||
if (!entityUuids.contains(resource.getUuid())) {
|
|
||||||
final var existingContextEntity = em.find(HsCredentialsContextRealEntity.class, resource.getUuid());
|
|
||||||
if ( existingContextEntity == null ) {
|
|
||||||
throw new EntityNotFoundException(
|
|
||||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
|
||||||
"credentials uuid", resource.getUuid()));
|
|
||||||
}
|
|
||||||
if (!existingContextEntity.getType().equals(resource.getType()) &&
|
|
||||||
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
|
||||||
throw new EntityNotFoundException(
|
|
||||||
messageTranslator.translate("existing {0} does not match given resource {1}",
|
|
||||||
existingContextEntity, resource));
|
|
||||||
}
|
|
||||||
entities.add(existingContextEntity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,7 @@ import java.util.UUID;
|
|||||||
@Table(schema = "rbac", name = "subject_rv")
|
@Table(schema = "rbac", name = "subject_rv")
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@Builder
|
||||||
@ToString
|
@ToString
|
||||||
@Immutable
|
@Immutable
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
@@ -45,7 +45,7 @@ public interface RbacSubjectRepository extends Repository<RbacSubjectEntity, UUI
|
|||||||
rbacSubjectEntity.setUuid(UUID.randomUUID());
|
rbacSubjectEntity.setUuid(UUID.randomUUID());
|
||||||
}
|
}
|
||||||
insert(rbacSubjectEntity);
|
insert(rbacSubjectEntity);
|
||||||
return rbacSubjectEntity;
|
return rbacSubjectEntity; // Not yet attached to EM!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Timed("app.rbac.subjects.repo.deleteByUuid")
|
@Timed("app.rbac.subjects.repo.deleteByUuid")
|
||||||
|
@@ -2,7 +2,7 @@ get:
|
|||||||
summary: Returns a list of all accessible contexts.
|
summary: Returns a list of all accessible contexts.
|
||||||
description: Returns the list of all contexts which are visible to the current subject or any of it's assumed roles.
|
description: Returns the list of all contexts which are visible to the current subject or any of it's assumed roles.
|
||||||
tags:
|
tags:
|
||||||
- -contexts
|
- contexts
|
||||||
operationId: getListOfContexts
|
operationId: getListOfContexts
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
|
@@ -9,6 +9,11 @@ components:
|
|||||||
uuid:
|
uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
person:
|
||||||
|
$ref: '../hs-office/hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||||
|
nickname:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname
|
||||||
totpSecret:
|
totpSecret:
|
||||||
type: string
|
type: string
|
||||||
telephonePassword:
|
telephonePassword:
|
||||||
@@ -64,9 +69,12 @@ components:
|
|||||||
CredentialsInsert:
|
CredentialsInsert:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
uuid:
|
person.uuid:
|
||||||
type: string
|
type: string
|
||||||
format: uuid
|
format: uuid
|
||||||
|
nickname:
|
||||||
|
type: string
|
||||||
|
pattern: '^[a-z][a-z0-9]{1,8}-[a-z0-9]{1,10}$' # TODO.spec: pattern for login nickname
|
||||||
totpSecret:
|
totpSecret:
|
||||||
type: string
|
type: string
|
||||||
telephonePassword:
|
telephonePassword:
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
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:
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
description: 'Fetch a single credentials its uuid, if visible for the current subject.'
|
description: 'Fetch a single credentials its uuid, if visible for the current subject.'
|
||||||
operationId: getSingleCredentialsByUuid
|
operationId: getSingleCredentialsByUuid
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: CredentialsUuid
|
- name: credentialsUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
@@ -27,12 +27,12 @@ get:
|
|||||||
|
|
||||||
patch:
|
patch:
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.'
|
description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.'
|
||||||
operationId: patchCredentials
|
operationId: patchCredentials
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
- name: CredentialsUuid
|
- name: credentialsUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
@@ -57,7 +57,7 @@ patch:
|
|||||||
|
|
||||||
delete:
|
delete:
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.'
|
description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.'
|
||||||
operationId: deleteCredentialsByUuid
|
operationId: deleteCredentialsByUuid
|
||||||
parameters:
|
parameters:
|
||||||
|
@@ -2,7 +2,7 @@ get:
|
|||||||
summary: Returns a list of all credentials.
|
summary: Returns a list of all credentials.
|
||||||
description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles.
|
description: Returns the list of all credentials which are visible to the current subject or any of it's assumed roles.
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
operationId: getListOfCredentialsByPersonUuid
|
operationId: getListOfCredentialsByPersonUuid
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
@@ -30,7 +30,7 @@ get:
|
|||||||
post:
|
post:
|
||||||
summary: Adds a new credentials.
|
summary: Adds a new credentials.
|
||||||
tags:
|
tags:
|
||||||
- -credentials
|
- credentials
|
||||||
operationId: postNewCredentials
|
operationId: postNewCredentials
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
|
@@ -50,6 +50,7 @@ public class ArchitectureTest {
|
|||||||
"..test.dom",
|
"..test.dom",
|
||||||
"..context",
|
"..context",
|
||||||
"..credentials",
|
"..credentials",
|
||||||
|
"..credentials.scenarios",
|
||||||
"..hash",
|
"..hash",
|
||||||
"..lambda",
|
"..lambda",
|
||||||
"..journal",
|
"..journal",
|
||||||
|
@@ -4,9 +4,13 @@ import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
|||||||
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
import net.hostsharing.hsadminng.config.JsonObjectMapperConfiguration;
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
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.HsOfficePersonRbacRepository;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||||
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||||
import org.hamcrest.CustomMatcher;
|
import org.hamcrest.CustomMatcher;
|
||||||
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;
|
||||||
@@ -37,6 +41,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
class HsCredentialsControllerRestTest {
|
class HsCredentialsControllerRestTest {
|
||||||
|
|
||||||
|
private static final UUID PERSON_UUID = UUID.randomUUID();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
MockMvc mockMvc;
|
MockMvc mockMvc;
|
||||||
|
|
||||||
@@ -54,7 +60,13 @@ class HsCredentialsControllerRestTest {
|
|||||||
EntityManagerFactory emf;
|
EntityManagerFactory emf;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
HsOfficePersonRbacRepository personRbacRepo;
|
RbacSubjectRepository subjectRepo;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HsOfficePersonRealRepository realPersonRepo;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
HsOfficePersonRbacRepository rbacPersonRepo;
|
||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
HsCredentialsContextRbacRepository loginContextRbacRepo;
|
||||||
@@ -62,6 +74,9 @@ class HsCredentialsControllerRestTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
HsCredentialsRepository credentialsRepo;
|
HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
CredentialContextResourceToEntityMapper contextMapper;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void patchCredentialsUsed() throws Exception {
|
void patchCredentialsUsed() throws Exception {
|
||||||
|
|
||||||
@@ -70,6 +85,8 @@ class HsCredentialsControllerRestTest {
|
|||||||
when(credentialsRepo.findByUuid(givenCredentialsUuid)).thenReturn(Optional.of(
|
when(credentialsRepo.findByUuid(givenCredentialsUuid)).thenReturn(Optional.of(
|
||||||
HsCredentialsEntity.builder()
|
HsCredentialsEntity.builder()
|
||||||
.uuid(givenCredentialsUuid)
|
.uuid(givenCredentialsUuid)
|
||||||
|
.person(HsOfficePersonRbacEntity.builder().uuid(PERSON_UUID).build())
|
||||||
|
.subject(RbacSubjectEntity.builder().name("some-nickname").build())
|
||||||
.lastUsed(null)
|
.lastUsed(null)
|
||||||
.onboardingToken("fake-onboarding-token")
|
.onboardingToken("fake-onboarding-token")
|
||||||
.build()
|
.build()
|
||||||
|
@@ -117,7 +117,8 @@ class HsCredentialsEntityPatcherUnitTest extends PatchUnitTestBase<
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) {
|
protected HsCredentialsEntityPatcher createPatcher(final HsCredentialsEntity entity) {
|
||||||
return new HsCredentialsEntityPatcher(em, mock(MessageTranslator.class), entity);
|
final var contextMapper = new CredentialContextResourceToEntityMapper(em, mock(MessageTranslator.class));
|
||||||
|
return new HsCredentialsEntityPatcher(contextMapper, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.credentials;
|
package net.hostsharing.hsadminng.credentials;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.context.Context;
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
@@ -51,8 +51,8 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
private RbacSubjectEntity alexSubject;
|
private RbacSubjectEntity alexSubject;
|
||||||
private RbacSubjectEntity drewSubject;
|
private RbacSubjectEntity drewSubject;
|
||||||
private RbacSubjectEntity testUserSubject;
|
private RbacSubjectEntity testUserSubject;
|
||||||
private HsOfficePersonRealEntity drewPerson;
|
private HsOfficePersonRbacEntity drewPerson;
|
||||||
private HsOfficePersonRealEntity testUserPerson;
|
private HsOfficePersonRbacEntity testUserPerson;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@@ -218,13 +218,13 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private HsOfficePersonRealEntity fetchPersonByGivenName(final String givenName) {
|
private HsOfficePersonRbacEntity fetchPersonByGivenName(final String givenName) {
|
||||||
final String jpql = "SELECT p FROM HsOfficePersonRealEntity p WHERE p.givenName = :givenName";
|
final String jpql = "SELECT p FROM HsOfficePersonRbacEntity p WHERE p.givenName = :givenName";
|
||||||
final Query query = em.createQuery(jpql, HsOfficePersonRealEntity.class);
|
final Query query = em.createQuery(jpql, HsOfficePersonRbacEntity.class);
|
||||||
query.setParameter("givenName", givenName);
|
query.setParameter("givenName", givenName);
|
||||||
try {
|
try {
|
||||||
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
context(SUPERUSER_ALEX_SUBJECT_NAME);
|
||||||
return notNull((HsOfficePersonRealEntity) query.getSingleResult());
|
return notNull((HsOfficePersonRbacEntity) 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);
|
||||||
|
@@ -0,0 +1,63 @@
|
|||||||
|
package net.hostsharing.hsadminng.credentials.scenarios;
|
||||||
|
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import static io.restassured.http.ContentType.JSON;
|
||||||
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
|
public class CreateCredentials extends UseCase<CreateCredentials> {
|
||||||
|
|
||||||
|
public CreateCredentials(final ScenarioTest testSuite) {
|
||||||
|
super(testSuite);
|
||||||
|
|
||||||
|
introduction("A set of credentials contains the login data for an RBAC subject.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpResponse run() {
|
||||||
|
|
||||||
|
obtain("Person: %{personGivenName} %{personFamilyName}", () ->
|
||||||
|
httpGet("/api/hs/office/persons?name=%{personFamilyName}")
|
||||||
|
.expecting(OK).expecting(JSON),
|
||||||
|
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||||
|
"In real situations we have more precise measures to find the related person."
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
obtain("CredentialsContexts", () ->
|
||||||
|
httpGet("/api/hs/credentials/contexts").expecting(OK).expecting(JSON)
|
||||||
|
);
|
||||||
|
|
||||||
|
return obtain("newCredentials", () ->
|
||||||
|
httpPost("/api/hs/credentials/credentials", usingJsonBody("""
|
||||||
|
{
|
||||||
|
"person.uuid": ${Person: %{personGivenName} %{personFamilyName}},
|
||||||
|
"nickname": ${nickname},
|
||||||
|
"active": %{active},
|
||||||
|
"emailAddress": ${emailAddress},
|
||||||
|
"telephonePassword": ${telephonePassword},
|
||||||
|
"smsNumber": ${smsNumber},
|
||||||
|
"globalUid": %{globalUid},
|
||||||
|
"globalGid": %{globalGid},
|
||||||
|
"contexts": @{contexts}
|
||||||
|
}
|
||||||
|
"""))
|
||||||
|
.expecting(HttpStatus.CREATED).expecting(ContentType.JSON)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void verify(final UseCase<CreateCredentials>.HttpResponse response) {
|
||||||
|
verify(
|
||||||
|
"Verify the New Credentials",
|
||||||
|
() -> httpGet("/api/hs/credentials/credentials/%{newCredentials}")
|
||||||
|
.expecting(OK).expecting(JSON),
|
||||||
|
path("uuid").contains("%{newCredentials}"),
|
||||||
|
path("nickname").contains("%{nickname}"),
|
||||||
|
path("person.uuid").contains("%{Person: %{personGivenName} %{personFamilyName}}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,83 @@
|
|||||||
|
package net.hostsharing.hsadminng.credentials.scenarios;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.Produces;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.mapper.Array;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
|
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.ClassOrderer;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Tag;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestClassOrder;
|
||||||
|
import org.junit.jupiter.api.TestInfo;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag("scenarioTest")
|
||||||
|
@SpringBootTest(
|
||||||
|
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||||
|
classes = { HsadminNgApplication.class, DisableSecurityConfig.class, JpaAttempt.class },
|
||||||
|
properties = {
|
||||||
|
"spring.datasource.url=${HSADMINNG_POSTGRES_JDBC_URL:jdbc:tc:postgresql:15.5-bookworm:///scenariosTC}",
|
||||||
|
"spring.datasource.username=${HSADMINNG_POSTGRES_ADMIN_USERNAME:ADMIN}",
|
||||||
|
"spring.datasource.password=${HSADMINNG_POSTGRES_ADMIN_PASSWORD:password}",
|
||||||
|
"hsadminng.superuser=${HSADMINNG_SUPERUSER:superuser-alex@hostsharing.net}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
|
||||||
|
@ExtendWith(IgnoreOnFailureExtension.class)
|
||||||
|
class CredentialsScenarioTests extends ScenarioTest {
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@BeforeEach
|
||||||
|
protected void beforeScenario(final TestInfo testInfo) {
|
||||||
|
super.beforeScenario(testInfo);
|
||||||
|
}
|
||||||
|
@Nested
|
||||||
|
@Order(10)
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
class CredentialScenarios {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1010)
|
||||||
|
@Produces(explicitly = "Credentials@hsadmin: firby-susan", implicitly = { "Person: Susan Firby" })
|
||||||
|
void shouldCreateInitialCredentialsForExistingNaturalPerson() {
|
||||||
|
new CreateCredentials(scenarioTest)
|
||||||
|
// to find a specific existing person
|
||||||
|
.given("personFamilyName", "Firby")
|
||||||
|
.given("personGivenName", "Susan")
|
||||||
|
// a login name, to be stored in the new RBAC subject
|
||||||
|
.given("nickname", "firby-susan")
|
||||||
|
// initial credentials
|
||||||
|
.given("active", true)
|
||||||
|
.given("emailAddress", "susan.firby@example.com")
|
||||||
|
.given("telephonePassword", "securePass123")
|
||||||
|
.given("smsNumber", "+49123456789")
|
||||||
|
.given("globalUid", 21011)
|
||||||
|
.given("globalGid", 21011)
|
||||||
|
.given("contexts", Array.of(
|
||||||
|
Map.ofEntries(
|
||||||
|
// a hardcoded context from test-data
|
||||||
|
// TODO.impl: the uuid should be determined within CreateCredentials just by (HSDAMIN,prod)
|
||||||
|
Map.entry("uuid", "11111111-1111-1111-1111-111111111111"),
|
||||||
|
Map.entry("type", "HSADMIN"),
|
||||||
|
Map.entry("qualifier", "prod")
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.doRun()
|
||||||
|
.keep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,9 @@
|
|||||||
package net.hostsharing.hsadminng.hs.office.scenarios;
|
package net.hostsharing.hsadminng.hs.office.scenarios;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
import net.hostsharing.hsadminng.HsadminNgApplication;
|
import net.hostsharing.hsadminng.HsadminNgApplication;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData;
|
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AddPhoneNumberToContactData;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AmendContactData;
|
import net.hostsharing.hsadminng.hs.office.scenarios.contact.AmendContactData;
|
||||||
import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFromContactData;
|
import net.hostsharing.hsadminng.hs.office.scenarios.contact.RemovePhoneNumberFromContactData;
|
||||||
@@ -39,9 +42,11 @@ import net.hostsharing.hsadminng.hs.office.scenarios.subscription.UnsubscribeFro
|
|||||||
import net.hostsharing.hsadminng.hs.scenarios.Produces;
|
import net.hostsharing.hsadminng.hs.scenarios.Produces;
|
||||||
import net.hostsharing.hsadminng.hs.scenarios.Requires;
|
import net.hostsharing.hsadminng.hs.scenarios.Requires;
|
||||||
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.lambda.Reducer;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
|
import net.hostsharing.hsadminng.test.IgnoreOnFailureExtension;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.ClassOrderer;
|
import org.junit.jupiter.api.ClassOrderer;
|
||||||
import org.junit.jupiter.api.Disabled;
|
import org.junit.jupiter.api.Disabled;
|
||||||
import org.junit.jupiter.api.MethodOrderer;
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
@@ -50,8 +55,10 @@ import org.junit.jupiter.api.Order;
|
|||||||
import org.junit.jupiter.api.Tag;
|
import org.junit.jupiter.api.Tag;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.TestClassOrder;
|
import org.junit.jupiter.api.TestClassOrder;
|
||||||
|
import org.junit.jupiter.api.TestInfo;
|
||||||
import org.junit.jupiter.api.TestMethodOrder;
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.test.context.ActiveProfiles;
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
|
||||||
@@ -71,6 +78,30 @@ import org.springframework.test.context.ActiveProfiles;
|
|||||||
@ExtendWith(IgnoreOnFailureExtension.class)
|
@ExtendWith(IgnoreOnFailureExtension.class)
|
||||||
class HsOfficeScenarioTests extends ScenarioTest {
|
class HsOfficeScenarioTests extends ScenarioTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
HsOfficePersonRbacRepository personRepo;
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
@BeforeEach
|
||||||
|
protected void beforeScenario(final TestInfo testInfo) {
|
||||||
|
createHostsharingPerson();
|
||||||
|
super.beforeScenario(testInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createHostsharingPerson() {
|
||||||
|
jpaAttempt.transacted(() ->
|
||||||
|
{
|
||||||
|
context.define("superuser-alex@hostsharing.net");
|
||||||
|
putAlias(
|
||||||
|
"Person: Hostsharing eG",
|
||||||
|
personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream()
|
||||||
|
.map(HsOfficePersonRbacEntity::getUuid)
|
||||||
|
.reduce(Reducer::toSingleElement).orElseThrow()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@Order(10)
|
@Order(10)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
@@ -1,9 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.hs.scenarios;
|
package net.hostsharing.hsadminng.hs.scenarios;
|
||||||
|
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
|
||||||
import net.hostsharing.hsadminng.lambda.Reducer;
|
|
||||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
import net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver;
|
import net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver;
|
||||||
@@ -58,15 +55,11 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
Integer port;
|
Integer port;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
HsOfficePersonRbacRepository personRepo;
|
protected JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
@Autowired
|
|
||||||
JpaAttempt jpaAttempt;
|
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeScenario(final TestInfo testInfo) {
|
protected void beforeScenario(final TestInfo testInfo) {
|
||||||
createHostsharingPerson();
|
|
||||||
try {
|
try {
|
||||||
testInfo.getTestMethod().ifPresent(currentTestMethod -> {
|
testInfo.getTestMethod().ifPresent(currentTestMethod -> {
|
||||||
callRequiredProducers(currentTestMethod);
|
callRequiredProducers(currentTestMethod);
|
||||||
@@ -86,20 +79,6 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
testReport.close();
|
testReport.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createHostsharingPerson() {
|
|
||||||
jpaAttempt.transacted(() ->
|
|
||||||
{
|
|
||||||
context.define("superuser-alex@hostsharing.net");
|
|
||||||
putAlias(
|
|
||||||
"Person: Hostsharing eG",
|
|
||||||
personRepo.findPersonByOptionalNameLike("Hostsharing eG").stream()
|
|
||||||
.map(HsOfficePersonRbacEntity::getUuid)
|
|
||||||
.reduce(Reducer::toSingleElement).orElseThrow()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
private void callRequiredProducers(final Method currentTestMethod) {
|
private void callRequiredProducers(final Method currentTestMethod) {
|
||||||
final var testMethodRequires = Optional.of(currentTestMethod)
|
final var testMethodRequires = Optional.of(currentTestMethod)
|
||||||
@@ -200,15 +179,15 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
return alias;
|
return alias;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void putAlias(final String name, final UUID value) {
|
protected static void putAlias(final String name, final UUID value) {
|
||||||
aliases.put(name, value);
|
aliases.put(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void putProperty(final String name, final Object value) {
|
protected static void putProperty(final String name, final Object value) {
|
||||||
properties.put(name, (value instanceof String string) ? resolveTyped(string) : value);
|
properties.put(name, (value instanceof String string) ? resolveTyped(string) : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void removeProperty(final String propName) {
|
protected static void removeProperty(final String propName) {
|
||||||
properties.remove(propName);
|
properties.remove(propName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,8 +5,10 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -14,6 +16,8 @@ import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.D
|
|||||||
|
|
||||||
public class TemplateResolver {
|
public class TemplateResolver {
|
||||||
|
|
||||||
|
public static final String JSON_NULL_VALUE_TO_KEEP = "NULL";
|
||||||
|
|
||||||
public enum Resolver {
|
public enum Resolver {
|
||||||
DROP_COMMENTS, // deletes comments ('#{whatever}' -> '')
|
DROP_COMMENTS, // deletes comments ('#{whatever}' -> '')
|
||||||
KEEP_COMMENTS // keep comments ('#{whatever}' -> 'whatever')
|
KEEP_COMMENTS // keep comments ('#{whatever}' -> 'whatever')
|
||||||
@@ -44,6 +48,12 @@ public class TemplateResolver {
|
|||||||
return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
|
return value != null ? URLEncoder.encode(value.toString(), StandardCharsets.UTF_8) : "";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
JSON_ARRAY('@'){
|
||||||
|
@Override
|
||||||
|
String convert(final Object value, final Resolver resolver) {
|
||||||
|
return jsonArray(value);
|
||||||
|
}
|
||||||
|
},
|
||||||
COMMENT('#'){
|
COMMENT('#'){
|
||||||
@Override
|
@Override
|
||||||
String convert(final Object value, final Resolver resolver) {
|
String convert(final Object value, final Resolver resolver) {
|
||||||
@@ -102,13 +112,14 @@ public class TemplateResolver {
|
|||||||
.collect(Collectors.joining("\n"));
|
.collect(Collectors.joining("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static boolean keepLine(final String line) {
|
private static boolean keepLine(final String line) {
|
||||||
final var trimmed = line.trim();
|
final var trimmed = line.trim();
|
||||||
return !trimmed.endsWith("null,") && !trimmed.endsWith("null");
|
return !trimmed.endsWith("null,") && !trimmed.endsWith("null");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String keptNullValues(final String line) {
|
private static String keptNullValues(final String line) {
|
||||||
return line.replace(": NULL", ": null");
|
return line.replace(": "+ JSON_NULL_VALUE_TO_KEEP, ": null");
|
||||||
}
|
}
|
||||||
|
|
||||||
private String copy() {
|
private String copy() {
|
||||||
@@ -163,12 +174,10 @@ public class TemplateResolver {
|
|||||||
// => last alternative element in expression was null and not optional
|
// => last alternative element in expression was null and not optional
|
||||||
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
|
throw new IllegalStateException("Missing required value in property-chain: " + nameExpression);
|
||||||
});
|
});
|
||||||
|
} else if (properties.containsKey(nameExpression)) {
|
||||||
|
return properties.get(nameExpression);
|
||||||
} else {
|
} else {
|
||||||
final var val = properties.get(nameExpression);
|
throw new IllegalStateException("Missing required property: " + nameExpression);
|
||||||
if (val == null) {
|
|
||||||
throw new IllegalStateException("Missing required property: " + nameExpression);
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,19 +221,40 @@ public class TemplateResolver {
|
|||||||
|
|
||||||
private static String jsonQuoted(final Object value) {
|
private static String jsonQuoted(final Object value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
case null -> null;
|
case null -> "null";
|
||||||
case Boolean bool -> bool.toString();
|
case Boolean bool -> bool.toString();
|
||||||
case Number number -> number.toString();
|
case Number number -> number.toString();
|
||||||
case String string -> "\"" + string.replace("\n", "\\n") + "\"";
|
case String string -> "\"" + string.replace("\n", "\\n") + "\"";
|
||||||
default -> "\"" + value + "\"";
|
case UUID uuid -> "\"" + uuid + "\"";
|
||||||
|
default -> jsonObject(value);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String jsonObject(final Object value) {
|
private static String jsonObject(final Object value) {
|
||||||
return switch (value) {
|
return switch (value) {
|
||||||
case null -> null;
|
case null -> "null";
|
||||||
|
case Map<?, ?> map -> "{" + map.entrySet().stream()
|
||||||
|
.map(entry -> "\"" + entry.getKey() + "\": " + jsonQuoted(entry.getValue()))
|
||||||
|
.collect(Collectors.joining(", ")) + "}";
|
||||||
case String string -> "{" + string.replace("\n", " ") + "}";
|
case String string -> "{" + string.replace("\n", " ") + "}";
|
||||||
default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object");
|
default -> throw new IllegalArgumentException("can not format " + value.getClass() + " (" + value + ") as JSON object");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String jsonArray(final Object value) {
|
||||||
|
return switch (value) {
|
||||||
|
case null -> "null";
|
||||||
|
case Object[] array -> "[" + Arrays.stream(array)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(TemplateResolver::jsonQuoted)
|
||||||
|
.collect(Collectors.joining(", ")) + "]";
|
||||||
|
case Collection<?> collection -> "[" + collection.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(TemplateResolver::jsonQuoted)
|
||||||
|
.collect(Collectors.joining(", ")) + "]";
|
||||||
|
case String string -> "[" + string.replace("\n", " ") + "]";
|
||||||
|
default -> throw new IllegalArgumentException("Cannot format " + value.getClass() + " (" + value + ") as JSON array");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,9 @@ package net.hostsharing.hsadminng.hs.scenarios;
|
|||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||||
@@ -12,6 +15,12 @@ class TemplateResolverUnitTest {
|
|||||||
@Test
|
@Test
|
||||||
void resolveTemplate() {
|
void resolveTemplate() {
|
||||||
final var resolved = new TemplateResolver("""
|
final var resolved = new TemplateResolver("""
|
||||||
|
JSON arrays:
|
||||||
|
- arrayWithMixedValues: @{arrayWithMixedValues}
|
||||||
|
- arrayWithObjects: @{arrayWithObjects}
|
||||||
|
- emptyArray: @{emptyArray}
|
||||||
|
- nullArray: @{nullArray}
|
||||||
|
|
||||||
with optional JSON quotes:
|
with optional JSON quotes:
|
||||||
|
|
||||||
${boolean},
|
${boolean},
|
||||||
@@ -19,7 +28,7 @@ class TemplateResolverUnitTest {
|
|||||||
${simple placeholder},
|
${simple placeholder},
|
||||||
${nested %{name}},
|
${nested %{name}},
|
||||||
${with-special-chars}
|
${with-special-chars}
|
||||||
|
|
||||||
and without quotes:
|
and without quotes:
|
||||||
|
|
||||||
%{boolean},
|
%{boolean},
|
||||||
@@ -36,16 +45,34 @@ class TemplateResolverUnitTest {
|
|||||||
&{nested %{name}},
|
&{nested %{name}},
|
||||||
&{with-special-chars}
|
&{with-special-chars}
|
||||||
""",
|
""",
|
||||||
Map.ofEntries(
|
orderedMapOfElementsWithNullValues(
|
||||||
Map.entry("name", "placeholder"),
|
entry("arrayWithMixedValues", new Object[] { "some string", true, 1234, "another string" }),
|
||||||
Map.entry("boolean", true),
|
entry("arrayWithObjects", new Object[] {
|
||||||
Map.entry("numeric", 42),
|
orderedMapOfElementsWithNullValues(
|
||||||
Map.entry("simple placeholder", "einfach"),
|
Map.entry("name", "some name"),
|
||||||
Map.entry("nested placeholder", "verschachtelt"),
|
Map.entry("number", 12345)
|
||||||
Map.entry("with-special-chars", "3&3 AG")
|
),
|
||||||
|
orderedMapOfElementsWithNullValues(
|
||||||
|
Map.entry("name", "another name"),
|
||||||
|
Map.entry("number", 98765)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
entry("emptyArray", new Object[] {}),
|
||||||
|
entry("nullArray", null),
|
||||||
|
entry("name", "placeholder"),
|
||||||
|
entry("boolean", true),
|
||||||
|
entry("numeric", 42),
|
||||||
|
entry("simple placeholder", "einfach"),
|
||||||
|
entry("nested placeholder", "verschachtelt"),
|
||||||
|
entry("with-special-chars", "3&3 AG")
|
||||||
)).resolve(DROP_COMMENTS);
|
)).resolve(DROP_COMMENTS);
|
||||||
|
|
||||||
assertThat(resolved).isEqualTo("""
|
assertThat(resolved).isEqualTo("""
|
||||||
|
JSON arrays:
|
||||||
|
- arrayWithMixedValues: ["some string", true, 1234, "another string"]
|
||||||
|
- arrayWithObjects: [{"name": "some name", "number": 12345}, {"name": "another name", "number": 98765}]
|
||||||
|
- emptyArray: []
|
||||||
|
|
||||||
with optional JSON quotes:
|
with optional JSON quotes:
|
||||||
|
|
||||||
true,
|
true,
|
||||||
@@ -71,4 +98,20 @@ class TemplateResolverUnitTest {
|
|||||||
3%263+AG
|
3%263+AG
|
||||||
""".trim());
|
""".trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
private Map<String, Object> orderedMapOfElementsWithNullValues(
|
||||||
|
final Map.Entry<String, Object>... entries) {
|
||||||
|
final var map = new LinkedHashMap<String, Object>();
|
||||||
|
if (entries != null) {
|
||||||
|
Arrays.stream(entries)
|
||||||
|
.forEach(entry -> map.put(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AbstractMap.SimpleEntry<String, Object> entry(String key, Object value) {
|
||||||
|
return new AbstractMap.SimpleEntry<>(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -449,7 +449,7 @@ class RbacSubjectControllerAcceptanceTest {
|
|||||||
RbacSubjectEntity givenANewUser() {
|
RbacSubjectEntity givenANewUser() {
|
||||||
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
final var givenUserName = "test-user-" + System.currentTimeMillis() + "@example.com";
|
||||||
final var givenUser = jpaAttempt.transacted(() -> {
|
final var givenUser = jpaAttempt.transacted(() -> {
|
||||||
context.define(null);
|
context.define("superuser-alex@hostsharing.net");
|
||||||
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), givenUserName));
|
return rbacSubjectRepository.create(new RbacSubjectEntity(UUID.randomUUID(), givenUserName));
|
||||||
}).assumeSuccessful().returnedValue();
|
}).assumeSuccessful().returnedValue();
|
||||||
assertThat(rbacSubjectRepository.findByName(givenUser.getName())).isNotNull();
|
assertThat(rbacSubjectRepository.findByName(givenUser.getName())).isNotNull();
|
||||||
|
Reference in New Issue
Block a user