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
@@ -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)
@@ -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 {
@@ -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();
@@ -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);
@@ -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'
+3
View File
@@ -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
@@ -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