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");
|
||||
|
||||
@@ -8,6 +8,11 @@ servers:
|
||||
|
||||
paths:
|
||||
|
||||
# current
|
||||
|
||||
/api/hs/accounts/current:
|
||||
$ref: "current.yaml"
|
||||
|
||||
# Contexts
|
||||
|
||||
/api/hs/accounts/contexts:
|
||||
|
||||
@@ -21,7 +21,3 @@ components:
|
||||
type: boolean
|
||||
required:
|
||||
- uuid
|
||||
- type
|
||||
- qualifier
|
||||
- onlyForNaturalPersons
|
||||
- publicAccess
|
||||
|
||||
@@ -3,6 +3,16 @@ components:
|
||||
|
||||
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:
|
||||
type: object
|
||||
properties:
|
||||
@@ -101,7 +111,8 @@ components:
|
||||
items:
|
||||
$ref: 'context-schemas.yaml#/components/schemas/Context'
|
||||
required:
|
||||
- uuid
|
||||
- person.uuid
|
||||
- nickname
|
||||
- active
|
||||
additionalProperties: false
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ get:
|
||||
description: 'Fetch a single credentials its uuid, if visible for the current subject.'
|
||||
operationId: getSingleCredentialsByUuid
|
||||
parameters:
|
||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: credentialsUuid
|
||||
in: path
|
||||
required: true
|
||||
@@ -31,7 +30,6 @@ patch:
|
||||
description: 'Updates a single credentials identified by its uuid, if permitted for the current subject.'
|
||||
operationId: patchCredentials
|
||||
parameters:
|
||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: credentialsUuid
|
||||
in: path
|
||||
required: true
|
||||
@@ -61,8 +59,7 @@ delete:
|
||||
description: 'Delete a single credentials identified by its uuid, if permitted for the current subject.'
|
||||
operationId: deleteCredentialsByUuid
|
||||
parameters:
|
||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||
- name: CredentialsUuid
|
||||
- name: credentialsUuid
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
|
||||
@@ -32,8 +32,6 @@ post:
|
||||
tags:
|
||||
- credentials
|
||||
operationId: postNewCredentials
|
||||
parameters:
|
||||
- $ref: 'auth.yaml#/components/parameters/assumedRoles'
|
||||
requestBody:
|
||||
description: A JSON object describing the new credential.
|
||||
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:
|
||||
|
||||
responses:
|
||||
|
||||
@@ -12,6 +12,30 @@ get:
|
||||
schema:
|
||||
type: string
|
||||
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:
|
||||
"200":
|
||||
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:
|
||||
|
||||
/api/rbac/context:
|
||||
$ref: 'rbac-context.yaml'
|
||||
|
||||
/api/rbac/subjects:
|
||||
$ref: 'rbac-subjects.yaml'
|
||||
|
||||
|
||||
@@ -72,6 +72,9 @@ metrics:
|
||||
logging:
|
||||
level:
|
||||
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:
|
||||
# https://docs.spring.io/spring-boot/reference/features/logging.html
|
||||
|
||||
|
||||
+9
-1
@@ -12,6 +12,9 @@ declare
|
||||
personAlexUuid uuid;
|
||||
superuserFranSubjectUuid uuid;
|
||||
personFranUuid uuid;
|
||||
userDrewSubjectUuid uuid;
|
||||
personDrewUuid uuid;
|
||||
|
||||
|
||||
context_HSADMIN_prod hs_accounts.context;
|
||||
context_SSH_internal hs_accounts.context;
|
||||
@@ -26,6 +29,8 @@ begin
|
||||
personAlexUuid = (SELECT uuid FROM hs_office.person WHERE givenName='Alex');
|
||||
superuserFranSubjectUuid = (SELECT uuid FROM rbac.subject WHERE name='superuser-fran@hostsharing.net');
|
||||
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
|
||||
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)
|
||||
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'),
|
||||
( 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
|
||||
INSERT INTO hs_accounts.context_mapping (credentials_uuid, context_uuid) VALUES
|
||||
(superuserAlexSubjectUuid, context_HSADMIN_prod.uuid),
|
||||
(superuserFranSubjectUuid, context_HSADMIN_prod.uuid),
|
||||
(userDrewSubjectUuid, context_HSADMIN_prod.uuid),
|
||||
(superuserAlexSubjectUuid, context_SSH_internal.uuid),
|
||||
(superuserFranSubjectUuid, context_SSH_internal.uuid),
|
||||
(userDrewSubjectUuid, context_SSH_external.uuid),
|
||||
(superuserAlexSubjectUuid, context_MATRIX_internal.uuid),
|
||||
(superuserFranSubjectUuid, context_MATRIX_internal.uuid);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ but\ is=ist aber
|
||||
|
||||
# credentials validations
|
||||
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
|
||||
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.
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user