add /api/rbac/context + /api/hs/accounts/current endpoints (#189)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/189 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
@@ -74,8 +74,11 @@ function importLegacyData() {
|
|||||||
alias gw-importHostingAssets='importLegacyData importHostingAssets'
|
alias gw-importHostingAssets='importLegacyData importHostingAssets'
|
||||||
|
|
||||||
function gradlewBootRun() {
|
function gradlewBootRun() {
|
||||||
echo gw bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${1:-8080}"
|
local port=${1:-8080}
|
||||||
./gradlew bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${1:-8080}"
|
shift
|
||||||
|
local additional_args="$@"
|
||||||
|
echo gw bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${port} ${additional_args}"
|
||||||
|
./gradlew bootRun --args="--spring.profiles.active=dev,fakeCasAuthenticator,complete,test-data --server.port=${port} ${additional_args}"
|
||||||
}
|
}
|
||||||
alias gw-bootRun=gradlewBootRun
|
alias gw-bootRun=gradlewBootRun
|
||||||
|
|
||||||
@@ -155,4 +158,3 @@ source .environment
|
|||||||
|
|
||||||
alias scenario-reports-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@hsh03.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@hsh03.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
|
alias scenario-reports-upload='./gradlew scenarioTest convertMarkdownToHtml && ssh hsh03-hsngdev@hsh03.hostsharing.net "rm -f doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office/*.html" && scp build/doc/scenarios/*.html hsh03-hsngdev@hsh03.hostsharing.net:doms/hsngdev.hs-example.de/htdocs-ssl/scenarios/office'
|
||||||
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'
|
alias scenario-reports-open='open https://hsngdev.hs-example.de/scenarios/office'
|
||||||
|
|
||||||
|
|||||||
@@ -669,3 +669,13 @@ tasks.register("compile") {
|
|||||||
description = "Compiles main and test Java sources."
|
description = "Compiles main and test Java sources."
|
||||||
dependsOn("compileJava", "compileTestJava")
|
dependsOn("compileJava", "compileTestJava")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tasks.named<org.springframework.boot.gradle.tasks.run.BootRun>("bootRun") {
|
||||||
|
// Enable debug when the debug property is set
|
||||||
|
if (project.hasProperty("debug")) {
|
||||||
|
jvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005")
|
||||||
|
}
|
||||||
|
// Or always enable debug (remove the if condition)
|
||||||
|
// jvmArgs = listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005")
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ public class Context {
|
|||||||
define(currentSubject, null);
|
define(currentSubject, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = MANDATORY)
|
||||||
|
public void define() {
|
||||||
|
define(SecurityContextHolder.getContext().getAuthentication().getName(), null);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(propagation = MANDATORY)
|
@Transactional(propagation = MANDATORY)
|
||||||
public void define(final String currentSubject, final String assumedRoles) {
|
public void define(final String currentSubject, final String assumedRoles) {
|
||||||
define(toTask(request), toCurl(request), currentSubject, assumedRoles);
|
define(toTask(request), toCurl(request), currentSubject, assumedRoles);
|
||||||
@@ -86,7 +91,7 @@ public class Context {
|
|||||||
return (UUID) em.createNativeQuery("select rbac.currentSubjectUuid()", UUID.class).getSingleResult();
|
return (UUID) em.createNativeQuery("select rbac.currentSubjectUuid()", UUID.class).getSingleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] fetchAssumedRoles() {
|
public String[] fetchAssumedRolesNames() {
|
||||||
return (String[]) em.createNativeQuery("select base.assumedRoles() as roles", String[].class).getSingleResult();
|
return (String[]) em.createNativeQuery("select base.assumedRoles() as roles", String[].class).getSingleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +99,10 @@ public class Context {
|
|||||||
return (UUID[]) em.createNativeQuery("select rbac.currentSubjectOrAssumedRolesUuids() as uuids", UUID[].class).getSingleResult();
|
return (UUID[]) em.createNativeQuery("select rbac.currentSubjectOrAssumedRolesUuids() as uuids", UUID[].class).getSingleResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isGlobalAdmin() {
|
||||||
|
return (boolean) em.createNativeQuery("select rbac.isGlobalAdmin()", boolean.class).getSingleResult();
|
||||||
|
}
|
||||||
|
|
||||||
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
||||||
final Optional<StackWalker.StackFrame> caller =
|
final Optional<StackWalker.StackFrame> caller =
|
||||||
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
|||||||
|
|
||||||
@ControllerAdvice
|
@ControllerAdvice
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
// HOWTO handle exceptions to produce specific http error codes and sensible error messages
|
// HOWTO error handler mapping exceptions to specific http error codes and sensible error messages
|
||||||
public class RestResponseEntityExceptionHandler
|
public class RestResponseEntityExceptionHandler
|
||||||
extends ResponseEntityExceptionHandler {
|
extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
|
||||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
@@ -54,8 +55,8 @@ public class CredentialContextResourceToEntityMapper {
|
|||||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
||||||
"credentials uuid", resource.getUuid()));
|
"credentials uuid", resource.getUuid()));
|
||||||
}
|
}
|
||||||
if (!existingContextEntity.getType().equals(resource.getType()) &&
|
if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) ||
|
||||||
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
||||||
throw new EntityNotFoundException(
|
throw new EntityNotFoundException(
|
||||||
messageTranslator.translate("existing {0} does not match given resource {1}",
|
messageTranslator.translate("existing {0} does not match given resource {1}",
|
||||||
existingContextEntity, resource));
|
existingContextEntity, resource));
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import java.util.function.BiConsumer;
|
|||||||
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 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.RbacSubjectResource;
|
||||||
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.accounts.generated.api.v1.api.CredentialsApi;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.api.CredentialsApi;
|
||||||
@@ -16,6 +18,7 @@ import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsInse
|
|||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsPatchResource;
|
||||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.CredentialsResource;
|
import net.hostsharing.hsadminng.accounts.generated.api.v1.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.HsOfficePersonRbacRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
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;
|
||||||
@@ -29,6 +32,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import jakarta.validation.ValidationException;
|
||||||
|
|
||||||
import static java.util.Optional.ofNullable;
|
import static java.util.Optional.ofNullable;
|
||||||
import static java.util.Optional.of;
|
import static java.util.Optional.of;
|
||||||
@@ -61,13 +65,15 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepo;
|
private HsCredentialsRepository credentialsRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RbacSubjectRepository rbacSubjectRepo;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
||||||
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(
|
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(final UUID credentialsUuid) {
|
||||||
final String assumedRoles,
|
|
||||||
final UUID credentialsUuid) {
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
context.assumeRoles(assumedRoles);
|
|
||||||
|
|
||||||
final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
||||||
if (credentialsEntity.isEmpty()) {
|
if (credentialsEntity.isEmpty()) {
|
||||||
@@ -99,10 +105,9 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.postNewCredentials")
|
@Timed("app.credentials.credentials.postNewCredentials")
|
||||||
public ResponseEntity<CredentialsResource> postNewCredentials(
|
public ResponseEntity<CredentialsResource> postNewCredentials(
|
||||||
final String assumedRoles,
|
|
||||||
final CredentialsInsertResource body
|
final CredentialsInsertResource body
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
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());
|
final var newlySavedSubject = createSubject(body.getNickname());
|
||||||
@@ -110,6 +115,7 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
// 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(
|
final var newCredentialsEntity = mapper.map(
|
||||||
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||||
|
validate(newCredentialsEntity);
|
||||||
newCredentialsEntity.setSubject(newlySavedSubject);
|
newCredentialsEntity.setSubject(newlySavedSubject);
|
||||||
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
||||||
|
|
||||||
@@ -127,10 +133,13 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@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 UUID credentialsUuid) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
||||||
|
credentialsEntity.getLoginContexts().clear();
|
||||||
|
em.flush();
|
||||||
em.remove(credentialsEntity);
|
em.remove(credentialsEntity);
|
||||||
|
em.remove(credentialsEntity.getSubject());
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,11 +147,10 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@Timed("app.credentials.credentials.patchCredentials")
|
@Timed("app.credentials.credentials.patchCredentials")
|
||||||
public ResponseEntity<CredentialsResource> patchCredentials(
|
public ResponseEntity<CredentialsResource> patchCredentials(
|
||||||
final String assumedRoles,
|
|
||||||
final UUID credentialsUuid,
|
final UUID credentialsUuid,
|
||||||
final CredentialsPatchResource body
|
final CredentialsPatchResource body
|
||||||
) {
|
) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
|
||||||
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
final var current = credentialsRepo.findByUuid(credentialsUuid).orElseThrow();
|
||||||
|
|
||||||
@@ -154,6 +162,25 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
@Timed("app.credentials.credentials.getCurrentLoginUser")
|
||||||
|
public ResponseEntity<CurrentLoginUserResource> getCurrentLoginUser() {
|
||||||
|
|
||||||
|
// define a context without assumed roles, otherwise we cannot access the subject anymore
|
||||||
|
context.define();
|
||||||
|
|
||||||
|
// fetch the data
|
||||||
|
final var currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
|
final var currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
||||||
|
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
||||||
|
final var person = credentialsRepo.findByUuid(currentSubjectUuid).orElseThrow().getPerson();
|
||||||
|
|
||||||
|
// finally, return the result
|
||||||
|
final var result = currentLoginUserResponse(currentSubject, person, isGlobalAdmin);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Timed("app.credentials.credentials.credentialsUsed")
|
@Timed("app.credentials.credentials.credentialsUsed")
|
||||||
public ResponseEntity<CredentialsResource> credentialsUsed(
|
public ResponseEntity<CredentialsResource> credentialsUsed(
|
||||||
@@ -171,12 +198,25 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return ResponseEntity.ok(mapped);
|
return ResponseEntity.ok(mapped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validate(final HsCredentialsEntity newCredentialsEntity) {
|
||||||
|
// the referenced person must be represented by currently logged in person
|
||||||
|
final var personUuid = newCredentialsEntity.getPerson().getUuid();
|
||||||
|
final var representedPersonUuids = rbacPersonRepo.findPersonsrepresentedByPersonWithUuid(personUuid)
|
||||||
|
.stream().map(HsOfficePerson::getUuid).toList();
|
||||||
|
if ( !representedPersonUuids.contains(personUuid)) {
|
||||||
|
throw new ValidationException(
|
||||||
|
messageTranslator.translate(
|
||||||
|
"access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person",
|
||||||
|
personUuid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private RbacSubjectEntity createSubject(final String nickname) {
|
private RbacSubjectEntity createSubject(final String nickname) {
|
||||||
final var newRbacSubject = subjectRepo.create(new RbacSubjectEntity(null, nickname));
|
final var newRbacSubject = subjectRepo.create(new RbacSubjectEntity(null, nickname));
|
||||||
if(context.fetchCurrentSubject() == null) {
|
if(context.fetchCurrentSubject() == null) {
|
||||||
context.define("activate newly created self-servie subject", null, nickname, null);
|
context.define("activate newly created self-service subject", null, nickname, null);
|
||||||
}
|
}
|
||||||
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // attached to EM
|
return subjectRepo.findByUuid(newRbacSubject.getUuid()); // now attached to EM
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
private List<HsCredentialsEntity> findByPersonUuid(final UUID personUuid) {
|
||||||
@@ -189,6 +229,18 @@ public class HsCredentialsController implements CredentialsApi {
|
|||||||
return credentialsRepo.findByPerson(person);
|
return credentialsRepo.findByPerson(person);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private CurrentLoginUserResource currentLoginUserResponse(
|
||||||
|
final RbacSubjectEntity currentSubject,
|
||||||
|
final HsOfficePerson<?> person,
|
||||||
|
final boolean isGlobalAdmin) {
|
||||||
|
final var result = new CurrentLoginUserResource();
|
||||||
|
result.setSubject(mapper.map(currentSubject, RbacSubjectResource.class));
|
||||||
|
result.setPerson(mapper.map(person, HsOfficePersonResource.class));
|
||||||
|
result.setGlobalAdmin(isGlobalAdmin);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
final BiConsumer<HsCredentialsEntity, CredentialsResource> ENTITY_TO_RESOURCE_POSTMAPPER = (entity, resource) -> {
|
||||||
ofNullable(entity.getLastUsed()).ifPresent(
|
ofNullable(entity.getLastUsed()).ifPresent(
|
||||||
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
|
dt -> resource.setLastUsed(dt.atOffset(ZoneOffset.UTC)));
|
||||||
@@ -213,8 +265,6 @@ 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) -> {
|
||||||
|
|
||||||
// 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(
|
final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
||||||
() -> new EntityNotFoundException(
|
() -> new EntityNotFoundException(
|
||||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid())
|
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid())
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
private UUID uuid;
|
private UUID uuid;
|
||||||
|
|
||||||
@MapsId
|
@MapsId
|
||||||
@OneToOne(optional = false, fetch = FetchType.LAZY)
|
@OneToOne(optional = false, fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
@JoinColumn(name = "uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
|
@JoinColumn(name = "uuid", nullable = false, updatable = false, referencedColumnName = "uuid")
|
||||||
private RbacSubjectEntity subject;
|
private RbacSubjectEntity subject;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
|||||||
@Column
|
@Column
|
||||||
private String smsNumber;
|
private String smsNumber;
|
||||||
|
|
||||||
@OneToMany(fetch = FetchType.LAZY, cascade = { MERGE, REFRESH }, orphanRemoval = true)
|
@OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH })
|
||||||
@JoinTable(
|
@JoinTable(
|
||||||
name = "context_mapping", schema = "hs_accounts",
|
name = "context_mapping", schema = "hs_accounts",
|
||||||
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
|
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
|
||||||
|
|||||||
@@ -19,30 +19,22 @@ public interface HsCredentialsRepository extends Repository<HsCredentialsEntity,
|
|||||||
|
|
||||||
@Timed("app.login.credentials.repo.findByCurrentSubject")
|
@Timed("app.login.credentials.repo.findByCurrentSubject")
|
||||||
@Query(nativeQuery = true, value = """
|
@Query(nativeQuery = true, value = """
|
||||||
WITH RECURSIVE owned_persons AS (
|
WITH RECURSIVE
|
||||||
-- Start with the person linked to current subject's credentials
|
same_person AS (
|
||||||
SELECT p.uuid AS person_uuid
|
SELECT own_credentials.person_uuid
|
||||||
FROM hs_accounts.credentials c
|
FROM hs_accounts.credentials own_credentials
|
||||||
JOIN hs_office.person p ON p.uuid = c.person_uuid
|
WHERE own_credentials.uuid = rbac.currentSubjectUuid()
|
||||||
WHERE c.uuid = rbac.currentSubjectUuid()
|
),
|
||||||
|
represented_persons AS (
|
||||||
UNION
|
SELECT relation.anchorUuid person_uuid
|
||||||
|
FROM hs_office.relation relation
|
||||||
-- Add persons where the current person has OWNER role
|
WHERE relation.type = 'REPRESENTATIVE'
|
||||||
SELECT p.uuid AS person_uuid
|
AND relation.holderUuid IN (SELECT person_uuid FROM same_person)
|
||||||
FROM owned_persons op
|
|
||||||
CROSS JOIN hs_office.person p
|
|
||||||
WHERE rbac.isGranted(
|
|
||||||
rbac.currentSubjectUuid(),
|
|
||||||
rbac.findRoleId(
|
|
||||||
rbac.roleDescriptorOf('hs_office.person', p.uuid, 'OWNER'::rbac.RoleType, false)
|
|
||||||
)
|
)
|
||||||
)
|
SELECT DISTINCT credentials.*
|
||||||
)
|
FROM hs_accounts.credentials credentials
|
||||||
SELECT DISTINCT c.*
|
WHERE credentials.person_uuid IN (SELECT person_uuid FROM same_person)
|
||||||
FROM hs_accounts.credentials c
|
OR credentials.person_uuid IN (SELECT person_uuid FROM represented_persons)
|
||||||
WHERE c.uuid = rbac.currentSubjectUuid() -- Include current subject's own credentials
|
|
||||||
OR c.person_uuid IN (SELECT person_uuid FROM owned_persons) -- Include credentials of owned persons
|
|
||||||
""")
|
""")
|
||||||
List<HsCredentialsEntity> findByCurrentSubject();
|
List<HsCredentialsEntity> findByCurrentSubject();
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -35,10 +35,13 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
|||||||
@Timed("app.office.persons.api.getListOfPersons")
|
@Timed("app.office.persons.api.getListOfPersons")
|
||||||
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
|
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
|
||||||
final String assumedRoles,
|
final String assumedRoles,
|
||||||
final String name) {
|
final String name,
|
||||||
|
final UUID representedByPersonUuid) {
|
||||||
context.assumeRoles(assumedRoles);
|
context.assumeRoles(assumedRoles);
|
||||||
|
|
||||||
final var entities = personRepo.findPersonByOptionalNameLike(name);
|
final var entities = representedByPersonUuid != null
|
||||||
|
? personRepo.findPersonsrepresentedByPersonWithUuid(representedByPersonUuid)
|
||||||
|
: personRepo.findPersonByOptionalNameLike(name);
|
||||||
|
|
||||||
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
|
final var resources = mapper.mapList(entities, HsOfficePersonResource.class);
|
||||||
return ResponseEntity.ok(resources);
|
return ResponseEntity.ok(resources);
|
||||||
|
|||||||
+16
@@ -23,6 +23,22 @@ public interface HsOfficePersonRbacRepository extends Repository<HsOfficePersonR
|
|||||||
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.rbac")
|
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.rbac")
|
||||||
List<HsOfficePersonRbacEntity> findPersonByOptionalNameLike(String name);
|
List<HsOfficePersonRbacEntity> 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.rbac")
|
||||||
|
List<HsOfficePersonRbacEntity> findPersonsrepresentedByPersonWithUuid(UUID personUuid);
|
||||||
|
|
||||||
@Timed("app.office.persons.repo.save.rbac")
|
@Timed("app.office.persons.repo.save.rbac")
|
||||||
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
|
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
package net.hostsharing.hsadminng.persistence;
|
package net.hostsharing.hsadminng.persistence;
|
||||||
|
|
||||||
|
|
||||||
import org.hibernate.Hibernate;
|
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
public interface BaseEntity<T extends BaseEntity<?>> {
|
public interface BaseEntity<T extends BaseEntity<?>> extends ImmutableBaseEntity<T> {
|
||||||
UUID getUuid();
|
|
||||||
|
|
||||||
int getVersion();
|
int getVersion();
|
||||||
|
|
||||||
default T load() {
|
|
||||||
Hibernate.initialize(this);
|
|
||||||
//noinspection unchecked
|
|
||||||
return (T) this;
|
|
||||||
};
|
|
||||||
|
|
||||||
default T reload(final EntityManager em) {
|
default T reload(final EntityManager em) {
|
||||||
em.flush();
|
em.flush();
|
||||||
em.refresh(this);
|
em.refresh(this);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package net.hostsharing.hsadminng.persistence;
|
||||||
|
|
||||||
|
|
||||||
|
import org.hibernate.Hibernate;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ImmutableBaseEntity<T extends ImmutableBaseEntity<?>> {
|
||||||
|
UUID getUuid();
|
||||||
|
|
||||||
|
default T load() {
|
||||||
|
Hibernate.initialize(this);
|
||||||
|
//noinspection unchecked
|
||||||
|
return (T) this;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.context;
|
||||||
|
|
||||||
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.api.RbacContextApi;
|
||||||
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacContextResource;
|
||||||
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacRoleResource;
|
||||||
|
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
|
||||||
|
import net.hostsharing.hsadminng.rbac.role.RbacRoleEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@SecurityRequirement(name = "casTicket")
|
||||||
|
public class RbacContextController implements RbacContextApi {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StrictMapper mapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RbacSubjectRepository rbacSubjectRepo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RbacRoleRepository roleRepo;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Timed("app.rbac.current.api.getContext")
|
||||||
|
public ResponseEntity<RbacContextResource> getContext(final String roleNamesToAssume) {
|
||||||
|
|
||||||
|
// fetch subject data before assuming any roles; otherwise we might have no SELECT permission anymore
|
||||||
|
context.define();
|
||||||
|
final var currentSubjectUuid = context.fetchCurrentSubjectUuid();
|
||||||
|
final var currentSubject = rbacSubjectRepo.findByUuid(currentSubjectUuid);
|
||||||
|
if (currentSubject == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
final boolean isGlobalAdmin = context.isGlobalAdmin();
|
||||||
|
|
||||||
|
// now we can assume the roles
|
||||||
|
context.assumeRoles(roleNamesToAssume);
|
||||||
|
final var assumedRoles = roleRepo.fetchAssumedRoles();
|
||||||
|
|
||||||
|
// finally, return the result
|
||||||
|
final var result = rbacContextResponse(currentSubjectUuid, currentSubject, assumedRoles, isGlobalAdmin);
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RbacContextResource rbacContextResponse(
|
||||||
|
final UUID currentSubjectUuid,
|
||||||
|
final RbacSubjectEntity currentSubject,
|
||||||
|
final List<RbacRoleEntity> assumedRoles,
|
||||||
|
final boolean isGlobalAdmin) {
|
||||||
|
final var result = new RbacContextResource();
|
||||||
|
final var currentSubjectResource = new RbacSubjectResource();
|
||||||
|
currentSubjectResource.setUuid(currentSubjectUuid);
|
||||||
|
currentSubjectResource.setName(currentSubject.getName());
|
||||||
|
result.setSubject(currentSubjectResource);
|
||||||
|
result.setGlobalAdmin(isGlobalAdmin);
|
||||||
|
final var assumedRolesResource = mapper.mapList(assumedRoles, RbacRoleResource.class);
|
||||||
|
result.setAssumedRoles(assumedRolesResource);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.role;
|
package net.hostsharing.hsadminng.rbac.role;
|
||||||
|
|
||||||
import io.micrometer.core.annotation.Timed;
|
import io.micrometer.core.annotation.Timed;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
import org.springframework.data.repository.Repository;
|
import org.springframework.data.repository.Repository;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -22,4 +23,12 @@ public interface RbacRoleRepository extends Repository<RbacRoleEntity, UUID> {
|
|||||||
|
|
||||||
@Timed("app.rbac.roles.repo.findByRoleName")
|
@Timed("app.rbac.roles.repo.findByRoleName")
|
||||||
RbacRoleEntity findByRoleName(String roleName);
|
RbacRoleEntity findByRoleName(String roleName);
|
||||||
|
|
||||||
|
@Timed("app.rbac.roles.repo.fetchAssumedRoles")
|
||||||
|
@Query(value = """
|
||||||
|
SELECT rev.*, rev.objectTable||'#'||rev.objectIdName||':'||rev.roleType AS roleName
|
||||||
|
FROM rbac.role_ev rev
|
||||||
|
WHERE rev.uuid = ANY(rbac.currentSubjectOrAssumedRolesUuids())
|
||||||
|
""", nativeQuery = true)
|
||||||
|
List<RbacRoleEntity> fetchAssumedRoles();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.subject;
|
package net.hostsharing.hsadminng.rbac.subject;
|
||||||
|
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
|
import net.hostsharing.hsadminng.persistence.ImmutableBaseEntity;
|
||||||
import org.springframework.data.annotation.Immutable;
|
import org.springframework.data.annotation.Immutable;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
@@ -21,7 +22,7 @@ import java.util.UUID;
|
|||||||
@Immutable
|
@Immutable
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class RbacSubjectEntity {
|
public class RbacSubjectEntity implements ImmutableBaseEntity<RbacSubjectEntity> {
|
||||||
|
|
||||||
private static final int MAX_VALIDITY_DAYS = 21;
|
private static final int MAX_VALIDITY_DAYS = 21;
|
||||||
private static DateTimeFormatter DATE_FORMAT_WITH_FULLHOUR = DateTimeFormatter.ofPattern("MM-dd-yyyy HH");
|
private static DateTimeFormatter DATE_FORMAT_WITH_FULLHOUR = DateTimeFormatter.ofPattern("MM-dd-yyyy HH");
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ servers:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
|
|
||||||
|
# current
|
||||||
|
|
||||||
|
/api/hs/accounts/current:
|
||||||
|
$ref: "current.yaml"
|
||||||
|
|
||||||
# Contexts
|
# Contexts
|
||||||
|
|
||||||
/api/hs/accounts/contexts:
|
/api/hs/accounts/contexts:
|
||||||
|
|||||||
@@ -21,7 +21,3 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required:
|
required:
|
||||||
- uuid
|
- uuid
|
||||||
- type
|
|
||||||
- qualifier
|
|
||||||
- onlyForNaturalPersons
|
|
||||||
- publicAccess
|
|
||||||
|
|||||||
@@ -3,6 +3,16 @@ components:
|
|||||||
|
|
||||||
schemas:
|
schemas:
|
||||||
|
|
||||||
|
CurrentLoginUser:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
subject:
|
||||||
|
$ref: '../rbac/rbac-subject-schemas.yaml#/components/schemas/RbacSubject'
|
||||||
|
person:
|
||||||
|
$ref: '../hs-office/hs-office-person-schemas.yaml#/components/schemas/HsOfficePerson'
|
||||||
|
globalAdmin:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
Credentials:
|
Credentials:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -101,7 +111,8 @@ components:
|
|||||||
items:
|
items:
|
||||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||||
required:
|
required:
|
||||||
- uuid
|
- person.uuid
|
||||||
|
- nickname
|
||||||
- active
|
- active
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ get:
|
|||||||
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'
|
|
||||||
- name: credentialsUuid
|
- name: credentialsUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
@@ -31,7 +30,6 @@ patch:
|
|||||||
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'
|
|
||||||
- name: credentialsUuid
|
- name: credentialsUuid
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
@@ -61,8 +59,7 @@ delete:
|
|||||||
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:
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
- name: credentialsUuid
|
||||||
- name: CredentialsUuid
|
|
||||||
in: path
|
in: path
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ post:
|
|||||||
tags:
|
tags:
|
||||||
- credentials
|
- credentials
|
||||||
operationId: postNewCredentials
|
operationId: postNewCredentials
|
||||||
parameters:
|
|
||||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
|
||||||
requestBody:
|
requestBody:
|
||||||
description: A JSON object describing the new credential.
|
description: A JSON object describing the new credential.
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
get:
|
||||||
|
summary: Currently logged in user data.
|
||||||
|
description: Returns information about the currently logged in user.
|
||||||
|
tags:
|
||||||
|
- credentials
|
||||||
|
operationId: getCurrentLoginUser
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: 'credentials-schemas.yaml#/components/schemas/CurrentLoginUser'
|
||||||
|
"401":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
|
"403":
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
|
|
||||||
responses:
|
responses:
|
||||||
|
|||||||
@@ -12,6 +12,30 @@ get:
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
description: Prefix of caption to filter the results.
|
description: Prefix of caption to filter the results.
|
||||||
|
- name: representedByPersonUuid
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: if given, if only persons represented given person uuid should be returned
|
||||||
|
x-parameter-dependencies:
|
||||||
|
oneOf:
|
||||||
|
- properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
not:
|
||||||
|
required: [ representedByPersonUuid ]
|
||||||
|
- properties:
|
||||||
|
representedByPersonUuid:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
not:
|
||||||
|
required: [ name ]
|
||||||
|
- not:
|
||||||
|
anyOf:
|
||||||
|
- required: [ name ]
|
||||||
|
- required: [ representedByPersonUuid ]
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: OK
|
description: OK
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
|
||||||
|
components:
|
||||||
|
|
||||||
|
schemas:
|
||||||
|
|
||||||
|
RbacContext:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
subject:
|
||||||
|
$ref: 'rbac-subject-schemas.yaml#/components/schemas/RbacSubject'
|
||||||
|
assumedRoles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: 'rbac-role-schemas.yaml#/components/schemas/RbacRole'
|
||||||
|
globalAdmin:
|
||||||
|
type: boolean
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- rbac-context
|
||||||
|
description: Information about the current subject.
|
||||||
|
operationId: getContext
|
||||||
|
parameters:
|
||||||
|
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: OK
|
||||||
|
content:
|
||||||
|
'application/json':
|
||||||
|
schema:
|
||||||
|
$ref: 'rbac-context-schemas.yaml#/components/schemas/RbacContext'
|
||||||
|
'401':
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Unauthorized'
|
||||||
|
'403':
|
||||||
|
$ref: 'error-responses.yaml#/components/responses/Forbidden'
|
||||||
@@ -8,6 +8,9 @@ servers:
|
|||||||
|
|
||||||
paths:
|
paths:
|
||||||
|
|
||||||
|
/api/rbac/context:
|
||||||
|
$ref: 'rbac-context.yaml'
|
||||||
|
|
||||||
/api/rbac/subjects:
|
/api/rbac/subjects:
|
||||||
$ref: 'rbac-subjects.yaml'
|
$ref: 'rbac-subjects.yaml'
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,9 @@ metrics:
|
|||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.springframework.security: info
|
org.springframework.security: info
|
||||||
|
org.springframework.web: DEBUG
|
||||||
|
org.springframework.web.method.annotation: DEBUG
|
||||||
|
org.springframework.validation: DEBUG
|
||||||
# HOWTO configure logging, e.g. logging to a separate file, see:
|
# HOWTO configure logging, e.g. logging to a separate file, see:
|
||||||
# https://docs.spring.io/spring-boot/reference/features/logging.html
|
# https://docs.spring.io/spring-boot/reference/features/logging.html
|
||||||
|
|
||||||
|
|||||||
+9
-1
@@ -12,6 +12,9 @@ declare
|
|||||||
personAlexUuid uuid;
|
personAlexUuid uuid;
|
||||||
superuserFranSubjectUuid uuid;
|
superuserFranSubjectUuid uuid;
|
||||||
personFranUuid uuid;
|
personFranUuid uuid;
|
||||||
|
userDrewSubjectUuid uuid;
|
||||||
|
personDrewUuid uuid;
|
||||||
|
|
||||||
|
|
||||||
context_HSADMIN_prod hs_accounts.context;
|
context_HSADMIN_prod hs_accounts.context;
|
||||||
context_SSH_internal hs_accounts.context;
|
context_SSH_internal hs_accounts.context;
|
||||||
@@ -26,6 +29,8 @@ begin
|
|||||||
personAlexUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Alex');
|
personAlexUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Alex');
|
||||||
superuserFranSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='superuser-fran@hostsharing.net');
|
superuserFranSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='superuser-fran@hostsharing.net');
|
||||||
personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran');
|
personFranUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Fran');
|
||||||
|
userDrewSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='selfregistered-user-drew@hostsharing.org');
|
||||||
|
personDrewUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Drew');
|
||||||
|
|
||||||
-- Add test contexts
|
-- Add test contexts
|
||||||
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
|
INSERT INTO hs_accounts.context (uuid, type, qualifier, only_for_natural_persons, public_access) VALUES
|
||||||
@@ -65,14 +70,17 @@ begin
|
|||||||
-- Add test credentials (linking to assumed rbac.subject UUIDs)
|
-- Add test credentials (linking to assumed rbac.subject UUIDs)
|
||||||
INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secrets, phone_password, email_address, sms_number) VALUES
|
INSERT INTO hs_accounts.credentials (uuid, version, person_uuid, active, global_uid, global_gid, onboarding_token, totp_secrets, phone_password, email_address, sms_number) VALUES
|
||||||
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'),
|
( superuserAlexSubjectUuid, 0, personAlexUuid, true, 1001, 1001, 'token-abc', ARRAY['otp-secret-1a', 'otp-secret-1b'], 'phone-pw-1', 'alex@example.com', '111-222-3333'),
|
||||||
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666');
|
( superuserFranSubjectUuid, 0, personFranUuid, true, 1002, 1002, 'token-def', ARRAY['otp-secret-2'], 'phone-pw-2', 'fran@example.com', '444-555-6666'),
|
||||||
|
( userDrewSubjectUuid, 0, personDrewUuid, true, 1003, 1003, 'token-def', ARRAY['otp-secret-3'], 'phone-pw-3', 'drew@example.org', '999-888-7777');
|
||||||
|
|
||||||
-- Map credentials to contexts
|
-- Map credentials to contexts
|
||||||
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
|
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
|
||||||
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
|
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
|
||||||
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
|
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
|
||||||
|
(userDrewSubjectUuid, context_HSADMIN_prod.uuid),
|
||||||
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
|
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
|
||||||
(superuserFranSubjectUuid, context_SSH_internal.uuid),
|
(superuserFranSubjectUuid, context_SSH_internal.uuid),
|
||||||
|
(userDrewSubjectUuid, context_SSH_external.uuid),
|
||||||
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
|
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
|
||||||
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
|
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ but\ is=ist aber
|
|||||||
|
|
||||||
# credentials validations
|
# credentials validations
|
||||||
existing\ {0}\ does\ not\ match\ given\ resource\ {1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
existing\ {0}\ does\ not\ match\ given\ resource\ {1}=existierender Credentials-Context {0} passt nicht zum angegebenen {1}
|
||||||
|
access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person=Zugriff verweigert: personUuid "{0}" wird von der eingeloggten Person nicht repräsentiert
|
||||||
|
|
||||||
# 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}
|
for\ transactionType\={0},\ shareCount\ must\ be\ positive\ but\ is\ {1}=für transactionType={0}, muss shareCount positiv sein, ist aber {1}
|
||||||
|
|||||||
@@ -5,4 +5,5 @@
|
|||||||
# But in that case, you can NOT use a prefix - or the prefix would be shown to the user as well.
|
# But in that case, you can NOT use a prefix - or the prefix would be shown to the user as well.
|
||||||
# I'm not sure, though, if using the english default translations as keys is really a good idea.
|
# I'm not sure, though, if using the english default translations as keys is really a good idea.
|
||||||
|
|
||||||
|
# credentials validations
|
||||||
|
access-denied-personUuid-{0}-not-represented-by-currently-logged-in-person=access denied: personUuid "{0}" not represented by currently logged in person
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ public class ArchitectureTest {
|
|||||||
"..mapper",
|
"..mapper",
|
||||||
"..ping",
|
"..ping",
|
||||||
"..rbac",
|
"..rbac",
|
||||||
|
"..rbac.context",
|
||||||
"..rbac.generator",
|
"..rbac.generator",
|
||||||
"..rbac.subject",
|
"..rbac.subject",
|
||||||
"..rbac.grant",
|
"..rbac.grant",
|
||||||
@@ -238,7 +239,8 @@ public class ArchitectureTest {
|
|||||||
"..hs.office.debitor..",
|
"..hs.office.debitor..",
|
||||||
"..hs.office.membership..",
|
"..hs.office.membership..",
|
||||||
"..hs.migration..",
|
"..hs.migration..",
|
||||||
"..hs.hosting.asset.."
|
"..hs.hosting.asset..",
|
||||||
|
"..hs.accounts.."
|
||||||
);
|
);
|
||||||
|
|
||||||
@ArchTest
|
@ArchTest
|
||||||
|
|||||||
+141
-2
@@ -3,6 +3,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
|||||||
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
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.config.MessagesResourceConfig;
|
||||||
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.HsOfficePersonRbacEntity;
|
||||||
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacRepository;
|
||||||
@@ -26,20 +27,31 @@ import jakarta.persistence.EntityManagerFactory;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZonedDateTime;
|
import java.time.ZonedDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
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.LEGAL_PERSON;
|
||||||
|
import static net.hostsharing.hsadminng.hs.office.person.HsOfficePersonType.NATURAL_PERSON;
|
||||||
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
import static net.hostsharing.hsadminng.test.JsonMatcher.lenientlyEquals;
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
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.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@WebMvcTest(HsCredentialsController.class)
|
@WebMvcTest(HsCredentialsController.class)
|
||||||
@Import({ StrictMapper.class, JsonObjectMapperConfiguration.class, DisableSecurityConfig.class, MessageTranslator.class })
|
@Import({
|
||||||
|
StrictMapper.class,
|
||||||
|
JsonObjectMapperConfiguration.class,
|
||||||
|
DisableSecurityConfig.class,
|
||||||
|
// HOWTO: test i18n translations
|
||||||
|
MessagesResourceConfig.class,
|
||||||
|
MessageTranslator.class })
|
||||||
@ActiveProfiles("test")
|
@ActiveProfiles("test")
|
||||||
class HsCredentialsControllerRestTest {
|
class HsCredentialsControllerRestTest {
|
||||||
|
|
||||||
@@ -79,6 +91,45 @@ class HsCredentialsControllerRestTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
CredentialContextResourceToEntityMapper contextMapper;
|
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
|
@Test
|
||||||
void shouldFilterInvalidContextsRegardingNonNaturalPerson() throws Exception {
|
void shouldFilterInvalidContextsRegardingNonNaturalPerson() throws Exception {
|
||||||
// given
|
// given
|
||||||
@@ -123,7 +174,95 @@ class HsCredentialsControllerRestTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void patchCredentialsUsed() throws Exception {
|
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
|
// given
|
||||||
final var givenCredentialsUuid = UUID.randomUUID();
|
final var givenCredentialsUuid = UUID.randomUUID();
|
||||||
|
|||||||
+156
-13
@@ -1,9 +1,14 @@
|
|||||||
package net.hostsharing.hsadminng.hs.accounts;
|
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.person.HsOfficePersonRbacEntity;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRbacEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.person.HsOfficePersonRealRepository;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationRealEntity;
|
||||||
|
import net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType;
|
||||||
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.test.ContextBasedTestWithCleanup;
|
||||||
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
|
||||||
import org.hibernate.TransientObjectException;
|
import org.hibernate.TransientObjectException;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
@@ -22,15 +27,18 @@ import java.time.ZonedDateTime;
|
|||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static net.hostsharing.hsadminng.hs.office.relation.HsOfficeRelationType.REPRESENTATIVE;
|
||||||
|
import static net.hostsharing.hsadminng.rbac.test.JpaAttempt.attempt;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||||
|
|
||||||
@DataJpaTest
|
@DataJpaTest
|
||||||
@Tag("generalIntegrationTest")
|
@Tag("generalIntegrationTest")
|
||||||
@Import({ Context.class, JpaAttempt.class })
|
@Import({ Context.class, JpaAttempt.class })
|
||||||
class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
class HsCredentialsRepositoryIntegrationTest extends ContextBasedTestWithCleanup {
|
||||||
|
|
||||||
private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
private static final String SUPERUSER_ALEX_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||||
|
private static final String SUPERUSER_FRAN_SUBJECT_NAME = "superuser-fran@hostsharing.net";
|
||||||
private static final String USER_DREW_SUBJECT_NAME = "selfregistered-user-drew@hostsharing.org";
|
private static final String USER_DREW_SUBJECT_NAME = "selfregistered-user-drew@hostsharing.org";
|
||||||
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
private static final String TEST_USER_SUBJECT_NAME = "selfregistered-test-user@hostsharing.org";
|
||||||
|
|
||||||
@@ -41,6 +49,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
@MockitoBean
|
@MockitoBean
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HsOfficePersonRealRepository personRepo;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HsCredentialsRepository credentialsRepository;
|
private HsCredentialsRepository credentialsRepository;
|
||||||
|
|
||||||
@@ -74,7 +85,9 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
final var rowsBefore = query.getResultList();
|
final var rowsBefore = query.getResultList();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsBefore).as("hs_accounts.credentials_hv only contain no rows for a timestamp before test data creation").hasSize(0);
|
assertThat(rowsBefore)
|
||||||
|
.as("hs_accounts.credentials_hv only contain no rows for a timestamp before test data creation")
|
||||||
|
.hasSize(0);
|
||||||
|
|
||||||
// and when
|
// and when
|
||||||
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
|
historicalContext(Timestamp.from(ZonedDateTime.now().toInstant()));
|
||||||
@@ -82,7 +95,53 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
final var rowsAfter = query.getResultList();
|
final var rowsAfter = query.getResultList();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(rowsAfter).as("hs_accounts.credentials_hv should now contain the test-data rows for the current timestamp").hasSize(2);
|
assertThat(rowsAfter)
|
||||||
|
.as("hs_accounts.credentials_hv should now contain the test-data rows for the current timestamp")
|
||||||
|
.hasSize(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void representativeShouldFindOwnAndRepresentedCredentialsByCurrentSubject() {
|
||||||
|
// given
|
||||||
|
final var firstGmbHPerson = givenPerson("First GmbH");
|
||||||
|
givenRelation(REPRESENTATIVE)
|
||||||
|
.withAnchorPersonLike(firstGmbHPerson)
|
||||||
|
.withHolder(drewPerson)
|
||||||
|
.withContact("some test contact");
|
||||||
|
givenCredentials()
|
||||||
|
.forSubject("first-gmbh")
|
||||||
|
.forPerson(firstGmbHPerson)
|
||||||
|
.withEMailAddress("first-gmbh@example.com");
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var foundCredentials = attempt(
|
||||||
|
em, () -> {
|
||||||
|
context(USER_DREW_SUBJECT_NAME);
|
||||||
|
return credentialsRepository.findByCurrentSubject();
|
||||||
|
})
|
||||||
|
.assertNotNull().returnedValue();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(foundCredentials).hasSize(2)
|
||||||
|
.map(HsCredentialsEntity::getEmailAddress)
|
||||||
|
.containsExactlyInAnyOrder("drew@example.org", "first-gmbh@example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void globalAdminShouldFindOnlyOwnCredentialsByCurrentSubject() {
|
||||||
|
|
||||||
|
// when
|
||||||
|
final var foundCredentials = attempt(
|
||||||
|
em, () -> {
|
||||||
|
context(SUPERUSER_FRAN_SUBJECT_NAME);
|
||||||
|
return credentialsRepository.findByCurrentSubject();
|
||||||
|
})
|
||||||
|
.assertNotNull().returnedValue();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(foundCredentials).hasSize(1)
|
||||||
|
.map(HsCredentialsEntity::getEmailAddress)
|
||||||
|
.containsExactlyInAnyOrder("fran@example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -101,28 +160,28 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
final var existingContext = loginContextRealRepo.findByTypeAndQualifier("HSADMIN", "prod")
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
final var newCredentials = HsCredentialsEntity.builder()
|
final var newCredentials = HsCredentialsEntity.builder()
|
||||||
.subject(drewSubject)
|
.subject(testUserSubject)
|
||||||
.person(drewPerson)
|
.person(testUserPerson)
|
||||||
.active(true)
|
.active(true)
|
||||||
.emailAddress("drew.new@example.com")
|
.emailAddress("test-user@example.com")
|
||||||
.globalUid(2001)
|
.globalUid(2011)
|
||||||
.globalGid(2001)
|
.globalGid(2011)
|
||||||
.loginContexts(mutableSetOf(existingContext))
|
.loginContexts(mutableSetOf(existingContext))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// when
|
// when
|
||||||
credentialsRepository.save(newCredentials);
|
toCleanup(credentialsRepository.save(newCredentials));
|
||||||
em.flush();
|
em.flush();
|
||||||
em.clear();
|
em.clear();
|
||||||
|
|
||||||
// then
|
// then
|
||||||
final var foundEntityOptional = credentialsRepository.findByUuid(drewSubject.getUuid());
|
final var foundEntityOptional = credentialsRepository.findByUuid(testUserSubject.getUuid());
|
||||||
assertThat(foundEntityOptional).isPresent();
|
assertThat(foundEntityOptional).isPresent();
|
||||||
final var foundEntity = foundEntityOptional.get();
|
final var foundEntity = foundEntityOptional.get();
|
||||||
assertThat(foundEntity.getEmailAddress()).isEqualTo("drew.new@example.com");
|
assertThat(foundEntity.getEmailAddress()).isEqualTo("test-user@example.com");
|
||||||
assertThat(foundEntity.isActive()).isTrue();
|
assertThat(foundEntity.isActive()).isTrue();
|
||||||
assertThat(foundEntity.getVersion()).isEqualTo(0); // Initial version
|
assertThat(foundEntity.getVersion()).isEqualTo(0); // Initial version
|
||||||
assertThat(foundEntity.getGlobalUid()).isEqualTo(2001);
|
assertThat(foundEntity.getGlobalUid()).isEqualTo(2011);
|
||||||
|
|
||||||
assertThat(foundEntity.getLoginContexts()).hasSize(1)
|
assertThat(foundEntity.getLoginContexts()).hasSize(1)
|
||||||
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
.map(HsCredentialsContextRealEntity::toString).contains("loginContext(HSADMIN:prod:NP-ONLY:PUBLIC)");
|
||||||
@@ -240,4 +299,88 @@ class HsCredentialsRepositoryIntegrationTest extends ContextBasedTest {
|
|||||||
private <T> Set<T> mutableSetOf(final T... elements) {
|
private <T> Set<T> mutableSetOf(final T... elements) {
|
||||||
return new HashSet<T>(Set.of(elements));
|
return new HashSet<T>(Set.of(elements));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HsOfficePersonRealEntity givenPerson(String personName) {
|
||||||
|
return personRepo.findPersonByOptionalNameLike(personName).getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RelationBuilder givenRelation(HsOfficeRelationType relationType) {
|
||||||
|
return new RelationBuilder(relationType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CredentialsBuilder givenCredentials() {
|
||||||
|
return new CredentialsBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class RelationBuilder {
|
||||||
|
private final HsOfficeRelationType relationType;
|
||||||
|
private HsOfficePersonRealEntity anchorPerson;
|
||||||
|
private HsOfficePersonRbacEntity holderPerson;
|
||||||
|
private HsOfficeContactRealEntity contact;
|
||||||
|
|
||||||
|
public RelationBuilder(HsOfficeRelationType relationType) {
|
||||||
|
this.relationType = relationType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RelationBuilder withAnchorPersonLike(HsOfficePersonRealEntity anchorPerson) {
|
||||||
|
this.anchorPerson = anchorPerson;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RelationBuilder withHolder(HsOfficePersonRbacEntity holderPerson) {
|
||||||
|
this.holderPerson = holderPerson;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsOfficeRelationRealEntity withContact(String caption) {
|
||||||
|
this.contact = HsOfficeContactRealEntity.builder()
|
||||||
|
.caption(caption)
|
||||||
|
.build();
|
||||||
|
em.persist(contact);
|
||||||
|
|
||||||
|
final var relation = HsOfficeRelationRealEntity.builder()
|
||||||
|
.type(relationType)
|
||||||
|
.anchor(anchorPerson)
|
||||||
|
.holder(em.getReference(HsOfficePersonRealEntity.class, holderPerson.getUuid()))
|
||||||
|
.contact(contact)
|
||||||
|
.build();
|
||||||
|
em.persist(relation);
|
||||||
|
em.flush();
|
||||||
|
return relation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CredentialsBuilder {
|
||||||
|
private RbacSubjectEntity subject;
|
||||||
|
private HsOfficePersonRealEntity person;
|
||||||
|
|
||||||
|
public CredentialsBuilder forSubject(String subjectName) {
|
||||||
|
this.subject = RbacSubjectEntity.builder()
|
||||||
|
.name(subjectName)
|
||||||
|
.build();
|
||||||
|
em.persist(subject);
|
||||||
|
toCleanup(subject);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CredentialsBuilder forPerson(HsOfficePersonRealEntity person) {
|
||||||
|
this.person = person;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HsCredentialsEntity withEMailAddress(String emailAddress) {
|
||||||
|
|
||||||
|
final var credentials = HsCredentialsEntity.builder()
|
||||||
|
.uuid(subject.getUuid())
|
||||||
|
.subject(subject)
|
||||||
|
.person(em.find(HsOfficePersonRbacEntity.class, person.getUuid()))
|
||||||
|
.emailAddress(emailAddress)
|
||||||
|
.active(true)
|
||||||
|
.build();
|
||||||
|
em.persist(credentials);
|
||||||
|
toCleanup(credentials);
|
||||||
|
em.flush();
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+36
@@ -49,6 +49,42 @@ class CredentialsScenarioTests extends ScenarioTest {
|
|||||||
@Nested
|
@Nested
|
||||||
@Order(10)
|
@Order(10)
|
||||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
class RbacContextScenarios {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(1010)
|
||||||
|
@Produces("RBAC Context")
|
||||||
|
void shouldFetchRbacContext() {
|
||||||
|
new FetchRbacContext(scenarioTest)
|
||||||
|
.given("subjectName", "superuser-fran@hostsharing.net")
|
||||||
|
.given("assumedRoles", "rbactest.package#xxx00:ADMIN;rbactest.package#yyy00:ADMIN")
|
||||||
|
.given("expectedToBeGlobalAdmin", true)
|
||||||
|
.doRun()
|
||||||
|
.keep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@Order(20)
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
class CurrentLoginUserScenarios {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2010)
|
||||||
|
@Produces("Current Login User")
|
||||||
|
void shouldFetchCurrentLoginUser() {
|
||||||
|
new CurrentLoginUser(scenarioTest)
|
||||||
|
.given("subjectName", "superuser-fran@hostsharing.net")
|
||||||
|
.given("personGivenName", "Fran")
|
||||||
|
.given("expectedToBeGlobalAdmin", true)
|
||||||
|
.doRun()
|
||||||
|
.keep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@Order(30)
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
class CredentialScenarios {
|
class CredentialScenarios {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||||
|
|
||||||
|
|
||||||
|
import static io.restassured.http.ContentType.JSON;
|
||||||
|
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolve;
|
||||||
|
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
|
public class CurrentLoginUser extends UseCase<CurrentLoginUser> {
|
||||||
|
|
||||||
|
public CurrentLoginUser(final ScenarioTest testSuite) {
|
||||||
|
super(testSuite);
|
||||||
|
|
||||||
|
introduction("Fetches data about the current login user.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpResponse run() {
|
||||||
|
|
||||||
|
obtain("Person: %{personGivenName}", () ->
|
||||||
|
httpGet("/api/hs/office/persons?name=" + uriEncoded("%{personGivenName}"))
|
||||||
|
.expecting(OK).expecting(JSON),
|
||||||
|
response -> response.expectArrayElements(1).getFromBody("[0].uuid"),
|
||||||
|
"In production, data this query could result in multiple outputs. In that case, you have to find out which is the right one."
|
||||||
|
);
|
||||||
|
|
||||||
|
return obtain(
|
||||||
|
"Current Login User", () ->
|
||||||
|
httpGet(
|
||||||
|
"/api/hs/accounts/current", req -> req
|
||||||
|
.header("Authorization", resolve("Bearer %{subjectName}", DROP_COMMENTS))
|
||||||
|
)
|
||||||
|
.expecting(OK).expecting(JSON).expectObject()
|
||||||
|
.extractValue("subject.name", "returnedSubjectName")
|
||||||
|
.extractValue("person.givenName", "returnedGivenName")
|
||||||
|
.extractValue("globalAdmin", "returnedGlobalAdmin")
|
||||||
|
).expecting(OK).expecting(JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void verify(final UseCase<CurrentLoginUser>.HttpResponse response) {
|
||||||
|
|
||||||
|
assertThat(resolve("%{returnedSubjectName}", DROP_COMMENTS))
|
||||||
|
.isEqualTo(resolve("%{subjectName}", DROP_COMMENTS));
|
||||||
|
|
||||||
|
assertThat(resolve("%{returnedGivenName}", DROP_COMMENTS))
|
||||||
|
.isEqualTo(resolve("%{personGivenName}", DROP_COMMENTS));
|
||||||
|
|
||||||
|
assertThat(resolve("%{returnedGlobalAdmin}", DROP_COMMENTS))
|
||||||
|
.isEqualTo(resolve("%{expectedToBeGlobalAdmin}", DROP_COMMENTS));
|
||||||
|
|
||||||
|
super.verify(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package net.hostsharing.hsadminng.hs.accounts.scenarios;
|
||||||
|
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.ScenarioTest;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.UseCase;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static io.restassured.http.ContentType.JSON;
|
||||||
|
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolve;
|
||||||
|
import static net.hostsharing.hsadminng.hs.scenarios.ScenarioTest.resolveJsonArray;
|
||||||
|
import static net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver.DROP_COMMENTS;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.http.HttpStatus.OK;
|
||||||
|
|
||||||
|
public class FetchRbacContext extends UseCase<FetchRbacContext> {
|
||||||
|
|
||||||
|
public FetchRbacContext(final ScenarioTest testSuite) {
|
||||||
|
super(testSuite);
|
||||||
|
|
||||||
|
introduction("Fetches the RBAC context for the login user / current subject.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected HttpResponse run() {
|
||||||
|
return obtain(
|
||||||
|
"RBAC Context", () ->
|
||||||
|
httpGet(
|
||||||
|
"/api/rbac/context", req -> req
|
||||||
|
.header("Authorization", resolve("Bearer %{subjectName}", DROP_COMMENTS))
|
||||||
|
.header("assumed-roles", resolve("%{assumedRoles}", DROP_COMMENTS))
|
||||||
|
)
|
||||||
|
.expecting(OK).expecting(JSON).expectObject()
|
||||||
|
.extractValue("subject.name", "returnedSubjectName")
|
||||||
|
.extractValue("assumedRoles", "returnedAssumedRoles")
|
||||||
|
.extractValue("globalAdmin", "returnedGlobalAdmin")
|
||||||
|
).expecting(OK).expecting(JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SneakyThrows
|
||||||
|
protected void verify(final UseCase<FetchRbacContext>.HttpResponse response) {
|
||||||
|
|
||||||
|
// HOWTO: assert in UseCase.verify()
|
||||||
|
|
||||||
|
assertThat(resolve("%{returnedSubjectName}", DROP_COMMENTS))
|
||||||
|
.isEqualTo(resolve("%{subjectName}", DROP_COMMENTS));
|
||||||
|
|
||||||
|
assertThat(resolveJsonArray("%{returnedAssumedRoles}")
|
||||||
|
.stream().map(m -> m.get("roleName")).toList())
|
||||||
|
.isEqualTo(List.of(resolve("%{assumedRoles}", DROP_COMMENTS).split(";")));
|
||||||
|
|
||||||
|
assertThat(resolve("%{returnedGlobalAdmin}", DROP_COMMENTS))
|
||||||
|
.isEqualTo(resolve("%{expectedToBeGlobalAdmin}", DROP_COMMENTS));
|
||||||
|
|
||||||
|
super.verify(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-6
@@ -78,6 +78,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
|
|
||||||
@MockitoBean
|
@MockitoBean
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class CreateDebitor {
|
class CreateDebitor {
|
||||||
|
|
||||||
@@ -242,9 +243,9 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
// then
|
// then
|
||||||
allTheseDebitorsAreReturned(
|
allTheseDebitorsAreReturned(
|
||||||
result,
|
result,
|
||||||
"debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)",
|
"debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)",
|
||||||
"debitor(D-1000212: rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='DEBITOR', holder='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.'), sec)",
|
"debitor(D-1000212: rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=DEBITOR, holder='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.'), sec)",
|
||||||
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
|
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ParameterizedTest
|
@ParameterizedTest
|
||||||
@@ -293,7 +294,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(result).map(Object::toString).contains(
|
assertThat(result).map(Object::toString).contains(
|
||||||
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
|
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,7 +311,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseDebitorsAreReturned(result,
|
exactlyTheseDebitorsAreReturned(result,
|
||||||
"debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
|
"debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +327,7 @@ class HsOfficeDebitorRepositoryIntegrationTest extends ContextBasedTestWithClean
|
|||||||
final var result = debitorRepo.findDebitorsByOptionalNameLike("third contact");
|
final var result = debitorRepo.findDebitorsByOptionalNameLike("third contact");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: rel(anchor='IF Third OHG', type='DEBITOR', holder='IF Third OHG'), thi)");
|
exactlyTheseDebitorsAreReturned(result, "debitor(D-1000313: rel(anchor='IF Third OHG', type=DEBITOR, holder='IF Third OHG'), thi)");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -413,7 +413,7 @@ class HsOfficePartnerControllerAcceptanceTest extends ContextBasedTestWithCleanu
|
|||||||
final var newPartnerPersonUuid = givenPartner.getPartnerRel().getHolder().getUuid();
|
final var newPartnerPersonUuid = givenPartner.getPartnerRel().getHolder().getUuid();
|
||||||
assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(newPartnerPersonUuid, EX_PARTNER, null, null, null))
|
assertThat(relationRepo.findRelationRelatedToPersonUuidRelationTypeMarkPersonAndContactData(newPartnerPersonUuid, EX_PARTNER, null, null, null))
|
||||||
.map(HsOfficeRelation::toShortString)
|
.map(HsOfficeRelation::toShortString)
|
||||||
.contains("rel(anchor='NP Winkler, Paul', type='EX_PARTNER', holder='UF Erben Bessler')");
|
.contains("rel(anchor='NP Winkler, Paul', type=EX_PARTNER, holder='UF Erben Bessler')");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+1
@@ -144,6 +144,7 @@ class HsOfficePersonEntityUnitTest {
|
|||||||
|
|
||||||
assertThat(actualDisplay).isEqualTo("person(salutation='Herr', title='Prof. Dr.', familyName='some family name', givenName='some given name')");
|
assertThat(actualDisplay).isEqualTo("person(salutation='Herr', title='Prof. Dr.', familyName='some family name', givenName='some given name')");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void toStringWithSalutationAndWithoutTitleSkipsTitle() {
|
void toStringWithSalutationAndWithoutTitleSkipsTitle() {
|
||||||
final var givenPersonEntity = HsOfficePersonRbacEntity.builder()
|
final var givenPersonEntity = HsOfficePersonRbacEntity.builder()
|
||||||
|
|||||||
+17
@@ -258,6 +258,23 @@ class HsOfficePersonRbacRepositoryIntegrationTest extends ContextBasedTestWithCl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void findPersonsrepresentedByPersonWithUuid() {
|
||||||
|
|
||||||
|
// given
|
||||||
|
context("superuser-alex@hostsharing.net");
|
||||||
|
final var personUuid = personRbacRepo.findPersonByOptionalNameLike("Fouler").getFirst().getUuid();
|
||||||
|
|
||||||
|
// when
|
||||||
|
@SuppressWarnings("unchecked") final List<HsOfficePersonRbacEntity> representedPersons = personRbacRepo.findPersonsrepresentedByPersonWithUuid(personUuid);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertThat(representedPersons).map(Object::toString).containsExactlyInAnyOrder(
|
||||||
|
"person(personType=NP, familyName='Fouler', givenName='Ellie')",
|
||||||
|
"person(personType=LP, tradeName='Fourth eG')"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void auditJournalLogIsAvailable() {
|
public void auditJournalLogIsAvailable() {
|
||||||
// given
|
// given
|
||||||
|
|||||||
+5
-5
@@ -62,10 +62,10 @@ class HsOfficeRealRelationRepositoryIntegrationTest extends ContextBasedTestWith
|
|||||||
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
|
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
|
||||||
exactlyTheseRelationsAreReturned(
|
exactlyTheseRelationsAreReturned(
|
||||||
result,
|
result,
|
||||||
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
|
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
|
||||||
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
|
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
|
||||||
"rel(anchor='NP Smith, Peter', type='DEBITOR', holder='NP Smith, Peter', contact='third contact')",
|
"rel(anchor='NP Smith, Peter', type=DEBITOR, holder='NP Smith, Peter', contact='third contact')",
|
||||||
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')"
|
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ class HsOfficeRealRelationRepositoryIntegrationTest extends ContextBasedTestWith
|
|||||||
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
|
context("superuser-alex@hostsharing.net"); // just to be able to access RBAc-entities persons+contact
|
||||||
exactlyTheseRelationsAreReturned(
|
exactlyTheseRelationsAreReturned(
|
||||||
result,
|
result,
|
||||||
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')"
|
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -126,7 +126,7 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
|
|||||||
final var stored = relationRbacRepo.findByUuid(result.returnedValue().getUuid());
|
final var stored = relationRbacRepo.findByUuid(result.returnedValue().getUuid());
|
||||||
assertThat(stored).isNotEmpty().map(HsOfficeRelation::toString).get()
|
assertThat(stored).isNotEmpty().map(HsOfficeRelation::toString).get()
|
||||||
.isEqualTo(
|
.isEqualTo(
|
||||||
"rel(anchor='UF Erben Bessler', type='SUBSCRIBER', mark='operations-announce', holder='NP Winkler, Paul', contact='fourth contact')");
|
"rel(anchor='UF Erben Bessler', type=SUBSCRIBER, mark='operations-announce', holder='NP Winkler, Paul', contact='fourth contact')");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -213,9 +213,9 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
|
|||||||
// then
|
// then
|
||||||
allTheseRelationsAreReturned(
|
allTheseRelationsAreReturned(
|
||||||
result,
|
result,
|
||||||
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
|
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
|
||||||
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
|
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
|
||||||
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')");
|
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -237,10 +237,10 @@ class HsOfficeRelationRepositoryIntegrationTest extends ContextBasedTestWithClea
|
|||||||
// then:
|
// then:
|
||||||
exactlyTheseRelationsAreReturned(
|
exactlyTheseRelationsAreReturned(
|
||||||
result,
|
result,
|
||||||
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type='REPRESENTATIVE', holder='NP Smith, Peter', contact='second contact')",
|
"rel(anchor='LP Peter Smith - The Second Hand and Thrift Stores-n-Shipping e.K.', type=REPRESENTATIVE, holder='NP Smith, Peter', contact='second contact')",
|
||||||
"rel(anchor='IF Third OHG', type='SUBSCRIBER', mark='members-announce', holder='NP Smith, Peter', contact='third contact')",
|
"rel(anchor='IF Third OHG', type=SUBSCRIBER, mark='members-announce', holder='NP Smith, Peter', contact='third contact')",
|
||||||
"rel(anchor='LP Hostsharing eG', type='PARTNER', holder='NP Smith, Peter', contact='sixth contact')",
|
"rel(anchor='LP Hostsharing eG', type=PARTNER, holder='NP Smith, Peter', contact='sixth contact')",
|
||||||
"rel(anchor='NP Smith, Peter', type='DEBITOR', holder='NP Smith, Peter', contact='third contact')");
|
"rel(anchor='NP Smith, Peter', type=DEBITOR, holder='NP Smith, Peter', contact='third contact')");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -383,7 +383,7 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
|
|||||||
context.define("superuser-alex@hostsharing.net");
|
context.define("superuser-alex@hostsharing.net");
|
||||||
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
|
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
|
||||||
.matches(mandate -> {
|
.matches(mandate -> {
|
||||||
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)");
|
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)");
|
||||||
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
|
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
|
||||||
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z - patched");
|
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z - patched");
|
||||||
assertThat(mandate.getValidFrom()).isEqualTo("2020-06-05");
|
assertThat(mandate.getValidFrom()).isEqualTo("2020-06-05");
|
||||||
@@ -424,7 +424,8 @@ class HsOfficeSepaMandateControllerAcceptanceTest extends ContextBasedTestWithCl
|
|||||||
// finally, the sepaMandate is actually updated
|
// finally, the sepaMandate is actually updated
|
||||||
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
|
assertThat(sepaMandateRepo.findByUuid(givenSepaMandate.getUuid())).isPresent().get()
|
||||||
.matches(mandate -> {
|
.matches(mandate -> {
|
||||||
assertThat(mandate.getDebitor().toString()).isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type='DEBITOR', holder='LP First GmbH'), fir)");
|
assertThat(mandate.getDebitor().toString())
|
||||||
|
.isEqualTo("debitor(D-1000111: rel(anchor='LP First GmbH', type=DEBITOR, holder='LP First GmbH'), fir)");
|
||||||
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
|
assertThat(mandate.getBankAccount().toShortString()).isEqualTo("First GmbH");
|
||||||
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z");
|
assertThat(mandate.getReference()).isEqualTo("temp ref CAT Z");
|
||||||
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)");
|
assertThat(mandate.getValidity().asString()).isEqualTo("[2022-11-01,2023-01-01)");
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package net.hostsharing.hsadminng.hs.scenarios;
|
package net.hostsharing.hsadminng.hs.scenarios;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import net.hostsharing.hsadminng.hs.scenarios.TemplateResolver.Resolver;
|
||||||
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 org.apache.commons.collections4.SetUtils;
|
import org.apache.commons.collections4.SetUtils;
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -17,6 +19,7 @@ import java.lang.reflect.Method;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Stack;
|
import java.util.Stack;
|
||||||
@@ -203,6 +206,14 @@ public abstract class ScenarioTest extends ContextBasedTest {
|
|||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public static List<Map<String, Object>> resolveJsonArray(final String text) {
|
||||||
|
return new ObjectMapper().readValue(
|
||||||
|
resolve(text, DROP_COMMENTS),
|
||||||
|
new TypeReference<>() {}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static Object resolveTyped(final String resolvableText) {
|
public static Object resolveTyped(final String resolvableText) {
|
||||||
final var resolved = resolve(resolvableText, DROP_COMMENTS);
|
final var resolved = resolve(resolvableText, DROP_COMMENTS);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -155,18 +155,26 @@ public abstract class UseCase<T extends UseCase<?>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
|
public final HttpResponse httpGet(
|
||||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
|
final String uriPathWithPlaceholder,
|
||||||
final var request = HttpRequest.newBuilder()
|
final Function<HttpRequest.Builder, HttpRequest.Builder> requestCustomizer) {
|
||||||
|
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholder, DROP_COMMENTS);
|
||||||
|
final var requestBuilder = HttpRequest.newBuilder()
|
||||||
.GET()
|
.GET()
|
||||||
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
.uri(new URI("http://localhost:" + testSuite.port + uriPath))
|
||||||
.header("Authorization", "Bearer " + ScenarioTest.RUN_AS_USER)
|
.timeout(seconds(HTTP_TIMEOUT_SECONDS));
|
||||||
.timeout(seconds(HTTP_TIMEOUT_SECONDS))
|
final var customizedRequestBuilder = requestCustomizer.apply(requestBuilder);
|
||||||
.build();
|
final var request = customizedRequestBuilder.build();
|
||||||
final var response = client.send(request, BodyHandlers.ofString());
|
final var response = client.send(request, BodyHandlers.ofString());
|
||||||
return new HttpResponse(HttpMethod.GET, uriPath, null, response);
|
return new HttpResponse(HttpMethod.GET, uriPath, null, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public final HttpResponse httpGet(final String uriPathWithPlaceholders) {
|
||||||
|
return httpGet(uriPathWithPlaceholders,
|
||||||
|
req -> req.header("Authorization", "Bearer " + ScenarioTest.RUN_AS_USER));
|
||||||
|
}
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
|
public final HttpResponse httpPost(final String uriPathWithPlaceholders, final JsonTemplate bodyJsonTemplate) {
|
||||||
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
|
final var uriPath = ScenarioTest.resolve(uriPathWithPlaceholders, DROP_COMMENTS);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class ContextIntegrationTests {
|
|||||||
|
|
||||||
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
|
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
|
||||||
|
|
||||||
assertThat(context.fetchAssumedRoles()).isEmpty();
|
assertThat(context.fetchAssumedRolesNames()).isEmpty();
|
||||||
|
|
||||||
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
|
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
|
||||||
.containsExactly(context.fetchCurrentSubjectUuid());
|
.containsExactly(context.fetchCurrentSubjectUuid());
|
||||||
@@ -90,7 +90,7 @@ class ContextIntegrationTests {
|
|||||||
|
|
||||||
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
|
assertThat(context.fetchCurrentSubjectUuid()).isNotNull();
|
||||||
|
|
||||||
assertThat(context.fetchAssumedRoles()).isEqualTo(Array.of("rbactest.package#yyy00:ADMIN"));
|
assertThat(context.fetchAssumedRolesNames()).isEqualTo(Array.of("rbactest.package#yyy00:ADMIN"));
|
||||||
|
|
||||||
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
|
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids())
|
||||||
.containsExactly(context.fetchCurrentSubjectOrAssumedRolesUuids());
|
.containsExactly(context.fetchCurrentSubjectOrAssumedRolesUuids());
|
||||||
@@ -133,7 +133,7 @@ class ContextIntegrationTests {
|
|||||||
assertThat(currentSubject).isEqualTo("superuser-alex@hostsharing.net");
|
assertThat(currentSubject).isEqualTo("superuser-alex@hostsharing.net");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assertThat(context.fetchAssumedRoles())
|
assertThat(context.fetchAssumedRolesNames())
|
||||||
.isEqualTo(Array.of("rbactest.customer#xxx:OWNER", "rbactest.customer#yyy:OWNER"));
|
.isEqualTo(Array.of("rbactest.customer#xxx:OWNER", "rbactest.customer#yyy:OWNER"));
|
||||||
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids()).hasSize(2);
|
assertThat(context.fetchCurrentSubjectOrAssumedRolesUuids()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package net.hostsharing.hsadminng.rbac.context;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.config.DisableSecurityConfig;
|
||||||
|
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||||
|
import net.hostsharing.hsadminng.context.Context;
|
||||||
|
import net.hostsharing.hsadminng.mapper.StrictMapper;
|
||||||
|
import net.hostsharing.hsadminng.persistence.EntityManagerWrapper;
|
||||||
|
import net.hostsharing.hsadminng.rbac.role.RbacRoleEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
|
||||||
|
import net.hostsharing.hsadminng.rbac.role.RbacRoleType;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectEntity;
|
||||||
|
import net.hostsharing.hsadminng.rbac.subject.RbacSubjectRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
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 jakarta.persistence.SynchronizationType;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.hasSize;
|
||||||
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
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(RbacContextController.class)
|
||||||
|
@Import({ StrictMapper.class, DisableSecurityConfig.class, MessageTranslator.class })
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
class RbacContextControllerRestTest {
|
||||||
|
|
||||||
|
private static final String GIVEN_SUBJECT_NAME = "superuser-alex@hostsharing.net";
|
||||||
|
private static final boolean GIVEN_GLOBAL_ADMIN = true;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
Context contextMock;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
RbacRoleRepository rbacRoleRepository;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
RbacSubjectRepository rbacSubjectRepository;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
EntityManagerWrapper em;
|
||||||
|
|
||||||
|
@MockitoBean
|
||||||
|
EntityManagerFactory emf;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() {
|
||||||
|
when(emf.createEntityManager()).thenReturn(em);
|
||||||
|
when(emf.createEntityManager(any(Map.class))).thenReturn(em);
|
||||||
|
when(emf.createEntityManager(any(SynchronizationType.class))).thenReturn(em);
|
||||||
|
when(emf.createEntityManager(any(SynchronizationType.class), any(Map.class))).thenReturn(em);
|
||||||
|
|
||||||
|
// current subject uuid mock
|
||||||
|
final var mockUuid = UUID.randomUUID();
|
||||||
|
when(contextMock.fetchCurrentSubjectUuid()).thenReturn(mockUuid);
|
||||||
|
|
||||||
|
// find by uuid mock
|
||||||
|
final var mockSubject = new RbacSubjectEntity();
|
||||||
|
mockSubject.setUuid(mockUuid);
|
||||||
|
mockSubject.setName(GIVEN_SUBJECT_NAME);
|
||||||
|
when(rbacSubjectRepository.findByUuid(mockUuid)).thenReturn(mockSubject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void apiContextWillReturnCurrentContext() throws Exception {
|
||||||
|
|
||||||
|
// given
|
||||||
|
final var rolesToAssume = "rbactest.package#xxx00:OWNER;rbactest.package#yyy00:OWNER";
|
||||||
|
when(contextMock.isGlobalAdmin()).thenReturn(GIVEN_GLOBAL_ADMIN);
|
||||||
|
when(rbacRoleRepository.fetchAssumedRoles()).thenReturn(
|
||||||
|
Arrays.stream(rolesToAssume.split(";"))
|
||||||
|
.map(RbacRoleDescriptor::fromRoleName)
|
||||||
|
.map(roleDesc -> new RbacRoleEntity(
|
||||||
|
UUID.randomUUID(), UUID.randomUUID(),
|
||||||
|
roleDesc.tableName, roleDesc.objectIdName, roleDesc.roleType,
|
||||||
|
roleDesc.roleName))
|
||||||
|
.toList()
|
||||||
|
);
|
||||||
|
|
||||||
|
// when
|
||||||
|
mockMvc.perform(MockMvcRequestBuilders
|
||||||
|
.get("/api/rbac/context")
|
||||||
|
.header("Authorization", "Bearer " + GIVEN_SUBJECT_NAME)
|
||||||
|
.header("assumed-roles", rolesToAssume)
|
||||||
|
.accept(MediaType.APPLICATION_JSON))
|
||||||
|
.andDo(print())
|
||||||
|
|
||||||
|
// then
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.subject.name", is(GIVEN_SUBJECT_NAME)))
|
||||||
|
.andExpect(jsonPath("$.globalAdmin", is(GIVEN_GLOBAL_ADMIN)))
|
||||||
|
.andExpect(jsonPath("$.assumedRoles", hasSize(2)))
|
||||||
|
.andExpect(jsonPath("$.assumedRoles[0].roleName", is("rbactest.package#xxx00:OWNER")))
|
||||||
|
.andExpect(jsonPath("$.assumedRoles[1].roleName", is("rbactest.package#yyy00:OWNER")));
|
||||||
|
}
|
||||||
|
|
||||||
|
record RbacRoleDescriptor(String roleName, String tableName, String objectIdName, RbacRoleType roleType) {
|
||||||
|
|
||||||
|
private static RbacRoleDescriptor fromRoleName(final String roleName) {
|
||||||
|
final var tablePlus = roleName.split("#");
|
||||||
|
final var tableName = tablePlus[0];
|
||||||
|
final var objectId = tablePlus[1].split(":")[0];
|
||||||
|
final var roleType = RbacRoleType.valueOf(tablePlus[1].split(":")[1]);
|
||||||
|
return new RbacRoleDescriptor(roleName, tableName, objectId, roleType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -100,6 +100,6 @@ class RbacGrantsDiagramServiceIntegrationTest extends ContextBasedTestWithCleanu
|
|||||||
final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office.coopassettx WHERE reference='ref 1000101-1'").getSingleResult();
|
final var targetObject = (UUID) em.createNativeQuery("SELECT uuid FROM hs_office.coopassettx WHERE reference='ref 1000101-1'").getSingleResult();
|
||||||
final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS));
|
final var graph = grantsMermaidService.allGrantsFrom(targetObject, "view", EnumSet.of(Include.USERS));
|
||||||
|
|
||||||
RbacGrantsDiagramService.writeToFile(join(";", context.fetchAssumedRoles()), graph, "doc/all-grants.md");
|
RbacGrantsDiagramService.writeToFile(join(";", context.fetchAssumedRolesNames()), graph, "doc/all-grants.md");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+25
@@ -177,6 +177,31 @@ class RbacRoleRepositoryIntegrationTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class FetchAssumedRoles {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void someSubject_withoutAssumedRole_fetchesNoAssumedRoles() {
|
||||||
|
context.define("customer-admin@xxx.example.com");
|
||||||
|
|
||||||
|
final var result = rbacRoleRepository.fetchAssumedRoles();
|
||||||
|
|
||||||
|
assertThat(result).isNotNull().hasSize(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void someSubject_withAssumedRoles_fetchesAssumedRoles() {
|
||||||
|
context.define("customer-admin@xxx.example.com",
|
||||||
|
"rbactest.package#xxx00:OWNER;rbactest.package#xxx01:OWNER;rbactest.package#xxx02:OWNER");
|
||||||
|
|
||||||
|
final var result = rbacRoleRepository.fetchAssumedRoles();
|
||||||
|
|
||||||
|
assertThat(result).isNotNull().hasSize(3)
|
||||||
|
.extracting(RbacRoleEntity::getRoleName)
|
||||||
|
.contains("rbactest.package#xxx00:OWNER", "rbactest.package#xxx01:OWNER", "rbactest.package#xxx02:OWNER");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void exactlyTheseRbacRolesAreReturned(final List<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
|
void exactlyTheseRbacRolesAreReturned(final List<RbacRoleEntity> actualResult, final String... expectedRoleNames) {
|
||||||
assertThat(actualResult)
|
assertThat(actualResult)
|
||||||
.extracting(RbacRoleEntity::getRoleName)
|
.extracting(RbacRoleEntity::getRoleName)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.hostsharing.hsadminng.rbac.test;
|
package net.hostsharing.hsadminng.rbac.test;
|
||||||
|
|
||||||
|
import net.hostsharing.hsadminng.persistence.ImmutableBaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
import net.hostsharing.hsadminng.rbac.context.ContextBasedTest;
|
||||||
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
import net.hostsharing.hsadminng.persistence.BaseEntity;
|
||||||
import net.hostsharing.hsadminng.rbac.grant.RbacGrantEntity;
|
import net.hostsharing.hsadminng.rbac.grant.RbacGrantEntity;
|
||||||
@@ -52,7 +53,7 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
@Autowired
|
@Autowired
|
||||||
JpaAttempt jpaAttempt;
|
JpaAttempt jpaAttempt;
|
||||||
|
|
||||||
private TreeMap<UUID, Class<? extends BaseEntity>> entitiesToCleanup = new TreeMap<>();
|
private TreeMap<UUID, Class<? extends ImmutableBaseEntity>> entitiesToCleanup = new TreeMap<>();
|
||||||
|
|
||||||
private static Long latestIntialTestDataSerialId;
|
private static Long latestIntialTestDataSerialId;
|
||||||
private static boolean countersInitialized = false;
|
private static boolean countersInitialized = false;
|
||||||
@@ -67,19 +68,19 @@ public abstract class ContextBasedTestWithCleanup extends ContextBasedTest {
|
|||||||
|
|
||||||
private TestInfo testInfo;
|
private TestInfo testInfo;
|
||||||
|
|
||||||
public <T extends BaseEntity> T refresh(final T entity) {
|
public <T extends ImmutableBaseEntity> T refresh(final T entity) {
|
||||||
final var merged = em.merge(entity);
|
final var merged = em.merge(entity);
|
||||||
em.refresh(merged);
|
em.refresh(merged);
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UUID toCleanup(final Class<? extends BaseEntity> entityClass, final UUID uuidToCleanup) {
|
public UUID toCleanup(final Class<? extends ImmutableBaseEntity> entityClass, final UUID uuidToCleanup) {
|
||||||
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup + ")");
|
out.println("toCleanup(" + entityClass.getSimpleName() + ", " + uuidToCleanup + ")");
|
||||||
entitiesToCleanup.put(uuidToCleanup, entityClass);
|
entitiesToCleanup.put(uuidToCleanup, entityClass);
|
||||||
return uuidToCleanup;
|
return uuidToCleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <E extends BaseEntity> E toCleanup(final E entity) {
|
public <E extends ImmutableBaseEntity> E toCleanup(final E entity) {
|
||||||
out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid());
|
out.println("toCleanup(" + entity.getClass() + ", " + entity.getUuid());
|
||||||
if ( entity.getUuid() == null ) {
|
if ( entity.getUuid() == null ) {
|
||||||
throw new IllegalArgumentException("only persisted entities with valid uuid allowed");
|
throw new IllegalArgumentException("only persisted entities with valid uuid allowed");
|
||||||
|
|||||||
Reference in New Issue
Block a user