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:
@@ -43,6 +43,11 @@ public class Context {
|
||||
define(currentSubject, null);
|
||||
}
|
||||
|
||||
@Transactional(propagation = MANDATORY)
|
||||
public void define() {
|
||||
define(SecurityContextHolder.getContext().getAuthentication().getName(), null);
|
||||
}
|
||||
|
||||
@Transactional(propagation = MANDATORY)
|
||||
public void define(final String currentSubject, final String 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();
|
||||
}
|
||||
|
||||
public String[] fetchAssumedRoles() {
|
||||
public String[] fetchAssumedRolesNames() {
|
||||
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();
|
||||
}
|
||||
|
||||
public boolean isGlobalAdmin() {
|
||||
return (boolean) em.createNativeQuery("select rbac.isGlobalAdmin()", boolean.class).getSingleResult();
|
||||
}
|
||||
|
||||
public static String getCallerMethodNameFromStackFrame(final int skipFrames) {
|
||||
final Optional<StackWalker.StackFrame> caller =
|
||||
StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ import static net.hostsharing.hsadminng.errors.CustomErrorResponse.*;
|
||||
|
||||
@ControllerAdvice
|
||||
@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
|
||||
extends ResponseEntityExceptionHandler {
|
||||
|
||||
|
||||
+3
-2
@@ -2,6 +2,7 @@ package net.hostsharing.hsadminng.hs.accounts;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
|
||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.ContextResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -54,8 +55,8 @@ public class CredentialContextResourceToEntityMapper {
|
||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible",
|
||||
"credentials uuid", resource.getUuid()));
|
||||
}
|
||||
if (!existingContextEntity.getType().equals(resource.getType()) &&
|
||||
!existingContextEntity.getQualifier().equals(resource.getQualifier())) {
|
||||
if ((resource.getType() != null && !existingContextEntity.getType().equals(resource.getType())) ||
|
||||
(resource.getQualifier() != null && !existingContextEntity.getQualifier().equals(resource.getQualifier()))) {
|
||||
throw new EntityNotFoundException(
|
||||
messageTranslator.translate("existing {0} does not match given resource {1}",
|
||||
existingContextEntity, resource));
|
||||
|
||||
@@ -32,9 +32,9 @@ public abstract class HsCredentialsContext implements Stringifyable, BaseEntity<
|
||||
private static Stringify<HsCredentialsContext> stringify = stringify(HsCredentialsContext.class, "loginContext")
|
||||
.withProp(HsCredentialsContext::getType)
|
||||
.withProp(HsCredentialsContext::getQualifier)
|
||||
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
|
||||
.withProp(HsCredentialsContext::isOnlyForNaturalPersons,
|
||||
value -> value ? symbol("NP-ONLY") : null)
|
||||
.withProp(HsCredentialsContext::isPublicAccess,
|
||||
.withProp(HsCredentialsContext::isPublicAccess,
|
||||
value -> value ? symbol("PUBLIC") : symbol("INTERNAL"))
|
||||
.quotedValues(false)
|
||||
.withSeparator(":");
|
||||
|
||||
@@ -9,6 +9,8 @@ import java.util.function.BiConsumer;
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
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.CurrentLoginUserResource;
|
||||
import net.hostsharing.hsadminng.accounts.generated.api.v1.model.RbacSubjectResource;
|
||||
import net.hostsharing.hsadminng.config.MessageTranslator;
|
||||
import net.hostsharing.hsadminng.context.Context;
|
||||
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.CredentialsResource;
|
||||
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.HsOfficePersonType;
|
||||
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 jakarta.persistence.EntityNotFoundException;
|
||||
import jakarta.validation.ValidationException;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static java.util.Optional.of;
|
||||
@@ -61,13 +65,15 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
@Autowired
|
||||
private HsCredentialsRepository credentialsRepo;
|
||||
|
||||
@Autowired
|
||||
private RbacSubjectRepository rbacSubjectRepo;
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true)
|
||||
@Timed("app.credentials.credentials.getSingleCredentialsByUuid")
|
||||
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(
|
||||
final String assumedRoles,
|
||||
final UUID credentialsUuid) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
public ResponseEntity<CredentialsResource> getSingleCredentialsByUuid(final UUID credentialsUuid) {
|
||||
|
||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||
|
||||
final var credentialsEntity = credentialsRepo.findByUuid(credentialsUuid);
|
||||
if (credentialsEntity.isEmpty()) {
|
||||
@@ -99,10 +105,9 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
@Transactional
|
||||
@Timed("app.credentials.credentials.postNewCredentials")
|
||||
public ResponseEntity<CredentialsResource> postNewCredentials(
|
||||
final String assumedRoles,
|
||||
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
|
||||
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
|
||||
final var newCredentialsEntity = mapper.map(
|
||||
body, HsCredentialsEntity.class, RESOURCE_TO_ENTITY_POSTMAPPER);
|
||||
validate(newCredentialsEntity);
|
||||
newCredentialsEntity.setSubject(newlySavedSubject);
|
||||
em.persist(newCredentialsEntity); // newCredentialsEntity.uuid == newlySavedSubject.uuid => do not use repository!
|
||||
|
||||
@@ -127,10 +133,13 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
@Override
|
||||
@Transactional
|
||||
@Timed("app.credentials.credentials.deleteCredentialsByUuid")
|
||||
public ResponseEntity<Void> deleteCredentialsByUuid(final String assumedRoles, final UUID credentialsUuid) {
|
||||
context.assumeRoles(assumedRoles);
|
||||
public ResponseEntity<Void> deleteCredentialsByUuid(final UUID credentialsUuid) {
|
||||
context.define(); // without assumed roles, otherwise we cannot access the subject anymore
|
||||
final var credentialsEntity = em.getReference(HsCredentialsEntity.class, credentialsUuid);
|
||||
credentialsEntity.getLoginContexts().clear();
|
||||
em.flush();
|
||||
em.remove(credentialsEntity);
|
||||
em.remove(credentialsEntity.getSubject());
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@@ -138,11 +147,10 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
@Transactional
|
||||
@Timed("app.credentials.credentials.patchCredentials")
|
||||
public ResponseEntity<CredentialsResource> patchCredentials(
|
||||
final String assumedRoles,
|
||||
final UUID credentialsUuid,
|
||||
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();
|
||||
|
||||
@@ -154,6 +162,25 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
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
|
||||
@Timed("app.credentials.credentials.credentialsUsed")
|
||||
public ResponseEntity<CredentialsResource> credentialsUsed(
|
||||
@@ -171,12 +198,25 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
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) {
|
||||
final var newRbacSubject = subjectRepo.create(new RbacSubjectEntity(null, nickname));
|
||||
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) {
|
||||
@@ -189,6 +229,18 @@ public class HsCredentialsController implements CredentialsApi {
|
||||
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) -> {
|
||||
ofNullable(entity.getLastUsed()).ifPresent(
|
||||
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) -> {
|
||||
|
||||
// TODO.impl: we need to make sure that the current subject is OWNER (or ADMIN?) of the person
|
||||
final var person = rbacPersonRepo.findByUuid(resource.getPersonUuid()).orElseThrow(
|
||||
() -> new EntityNotFoundException(
|
||||
messageTranslator.translate("{0} \"{1}\" not found or not accessible", "personUuid", resource.getPersonUuid())
|
||||
|
||||
@@ -40,7 +40,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
||||
private UUID uuid;
|
||||
|
||||
@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")
|
||||
private RbacSubjectEntity subject;
|
||||
|
||||
@@ -78,7 +78,7 @@ public class HsCredentialsEntity implements BaseEntity<HsCredentialsEntity>, Str
|
||||
@Column
|
||||
private String smsNumber;
|
||||
|
||||
@OneToMany(fetch = FetchType.LAZY, cascade = { MERGE, REFRESH }, orphanRemoval = true)
|
||||
@OneToMany(fetch = FetchType.EAGER, cascade = { MERGE, REFRESH })
|
||||
@JoinTable(
|
||||
name = "context_mapping", schema = "hs_accounts",
|
||||
joinColumns = @JoinColumn(name = "credentials_uuid", referencedColumnName = "uuid"),
|
||||
|
||||
@@ -19,30 +19,22 @@ public interface HsCredentialsRepository extends Repository<HsCredentialsEntity,
|
||||
|
||||
@Timed("app.login.credentials.repo.findByCurrentSubject")
|
||||
@Query(nativeQuery = true, value = """
|
||||
WITH RECURSIVE owned_persons AS (
|
||||
-- Start with the person linked to current subject's credentials
|
||||
SELECT p.uuid AS person_uuid
|
||||
FROM hs_accounts.credentials c
|
||||
JOIN hs_office.person p ON p.uuid = c.person_uuid
|
||||
WHERE c.uuid = rbac.currentSubjectUuid()
|
||||
|
||||
UNION
|
||||
|
||||
-- Add persons where the current person has OWNER role
|
||||
SELECT p.uuid AS person_uuid
|
||||
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)
|
||||
)
|
||||
WITH RECURSIVE
|
||||
same_person AS (
|
||||
SELECT own_credentials.person_uuid
|
||||
FROM hs_accounts.credentials own_credentials
|
||||
WHERE own_credentials.uuid = rbac.currentSubjectUuid()
|
||||
),
|
||||
represented_persons AS (
|
||||
SELECT relation.anchorUuid person_uuid
|
||||
FROM hs_office.relation relation
|
||||
WHERE relation.type = 'REPRESENTATIVE'
|
||||
AND relation.holderUuid IN (SELECT person_uuid FROM same_person)
|
||||
)
|
||||
)
|
||||
SELECT DISTINCT c.*
|
||||
FROM hs_accounts.credentials c
|
||||
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
|
||||
SELECT DISTINCT credentials.*
|
||||
FROM hs_accounts.credentials credentials
|
||||
WHERE credentials.person_uuid IN (SELECT person_uuid FROM same_person)
|
||||
OR credentials.person_uuid IN (SELECT person_uuid FROM represented_persons)
|
||||
""")
|
||||
List<HsCredentialsEntity> findByCurrentSubject();
|
||||
|
||||
|
||||
+5
-2
@@ -35,10 +35,13 @@ public class HsOfficePersonController implements HsOfficePersonsApi {
|
||||
@Timed("app.office.persons.api.getListOfPersons")
|
||||
public ResponseEntity<List<HsOfficePersonResource>> getListOfPersons(
|
||||
final String assumedRoles,
|
||||
final String name) {
|
||||
final String name,
|
||||
final UUID representedByPersonUuid) {
|
||||
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);
|
||||
return ResponseEntity.ok(resources);
|
||||
|
||||
+16
@@ -23,6 +23,22 @@ public interface HsOfficePersonRbacRepository extends Repository<HsOfficePersonR
|
||||
@Timed("app.office.persons.repo.findPersonByOptionalNameLike.rbac")
|
||||
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")
|
||||
HsOfficePersonRbacEntity save(final HsOfficePersonRbacEntity entity);
|
||||
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
package net.hostsharing.hsadminng.persistence;
|
||||
|
||||
|
||||
import org.hibernate.Hibernate;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface BaseEntity<T extends BaseEntity<?>> {
|
||||
UUID getUuid();
|
||||
public interface BaseEntity<T extends BaseEntity<?>> extends ImmutableBaseEntity<T> {
|
||||
|
||||
int getVersion();
|
||||
|
||||
default T load() {
|
||||
Hibernate.initialize(this);
|
||||
//noinspection unchecked
|
||||
return (T) this;
|
||||
};
|
||||
|
||||
default T reload(final EntityManager em) {
|
||||
em.flush();
|
||||
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;
|
||||
|
||||
import io.micrometer.core.annotation.Timed;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.Repository;
|
||||
|
||||
import java.util.List;
|
||||
@@ -22,4 +23,12 @@ public interface RbacRoleRepository extends Repository<RbacRoleEntity, UUID> {
|
||||
|
||||
@Timed("app.rbac.roles.repo.findByRoleName")
|
||||
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;
|
||||
|
||||
import lombok.*;
|
||||
import net.hostsharing.hsadminng.persistence.ImmutableBaseEntity;
|
||||
import org.springframework.data.annotation.Immutable;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
@@ -21,7 +22,7 @@ import java.util.UUID;
|
||||
@Immutable
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RbacSubjectEntity {
|
||||
public class RbacSubjectEntity implements ImmutableBaseEntity<RbacSubjectEntity> {
|
||||
|
||||
private static final int MAX_VALIDITY_DAYS = 21;
|
||||
private static DateTimeFormatter DATE_FORMAT_WITH_FULLHOUR = DateTimeFormatter.ofPattern("MM-dd-yyyy HH");
|
||||
|
||||
Reference in New Issue
Block a user