1
0

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:
Michael Hoennig
2025-08-21 12:45:59 +02:00
parent 60028697d6
commit 5a5c1466b0
51 changed files with 1034 additions and 129 deletions
+5 -3
View File
@@ -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'
+10
View File
@@ -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)
@@ -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 {
@@ -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.*
SELECT DISTINCT c.* FROM hs_accounts.credentials credentials
FROM hs_accounts.credentials c WHERE credentials.person_uuid IN (SELECT person_uuid FROM same_person)
WHERE c.uuid = rbac.currentSubjectUuid() -- Include current subject's own credentials OR credentials.person_uuid IN (SELECT person_uuid FROM represented_persons)
OR c.person_uuid IN (SELECT person_uuid FROM owned_persons) -- Include credentials of owned persons
""") """)
List<HsCredentialsEntity> findByCurrentSubject(); List<HsCredentialsEntity> findByCurrentSubject();
@@ -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);
@@ -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'
+3
View File
@@ -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
@@ -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
@@ -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();
@@ -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;
}
}
} }
@@ -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);
}
}
@@ -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)");
} }
} }
@@ -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
@@ -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()
@@ -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
@@ -265,7 +282,7 @@ class HsOfficePersonRbacRepositoryIntegrationTest extends ContextBasedTestWithCl
select currentTask, targetTable, targetOp, targetdelta->>'tradename', targetdelta->>'lastname' select currentTask, targetTable, targetOp, targetdelta->>'tradename', targetdelta->>'lastname'
from base.tx_journal_v from base.tx_journal_v
where targettable = 'hs_office.person'; where targettable = 'hs_office.person';
"""); """);
// when // when
@SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList(); @SuppressWarnings("unchecked") final List<Object[]> customerLogEntries = query.getResultList();
@@ -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')"
); );
} }
} }
@@ -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')");
} }
} }
@@ -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;
@@ -159,7 +162,7 @@ public abstract class ScenarioTest extends ContextBasedTest {
assertThat(knowVariables().containsKey(declaredAlias)) assertThat(knowVariables().containsKey(declaredAlias))
.as("@Producer method " + currentTestMethod.getName() + .as("@Producer method " + currentTestMethod.getName() +
" did declare but not produce \"" + declaredAlias + "\"") " did declare but not produce \"" + declaredAlias + "\"")
.isTrue() ); .isTrue());
} }
}); });
} }
@@ -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);
} }
@@ -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);
}
}
}
@@ -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");
} }
} }
@@ -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");