RbacContextController.java

package net.hostsharing.hsadminng.rbac.context;

import io.micrometer.core.annotation.Timed;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import net.hostsharing.hsadminng.config.ApiKeyAuthenticationFilter;
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.RbacContextApiKeyResource;
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.RbacSubjectGroupResource;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.RbacSubjectResource;
import net.hostsharing.hsadminng.rbac.generated.api.v1.model.SubjectTypeResource;
import net.hostsharing.hsadminng.rbac.role.RbacRoleEntity;
import net.hostsharing.hsadminng.rbac.role.RbacRoleRepository;
import net.hostsharing.hsadminng.rbac.subject.RealSubjectEntity;
import net.hostsharing.hsadminng.rbac.subject.RealSubjectRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;

import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

@RestController
@PreAuthorize("isAuthenticated()")
@SecurityRequirement(name = "bearerAuth")
public class RbacContextController implements RbacContextApi {

    @Autowired
    private Context context;

    @Autowired
    private StrictMapper mapper;

    @Autowired
    private RealSubjectRepository realSubjectRepo;

    @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 = realSubjectRepo.findCurrentSubject();
        if (currentSubject == null) {
            return ResponseEntity.notFound().build();
        }
        final boolean isGlobalAdmin = context.isGlobalAdmin();
        // claimed groups come straight from the JWT and are independent of any assumed role
        final var claimedGroups = context.fetchClaimedSubjectGroupNames();

        // now we can assume the roles
        context.assumeRoles(roleNamesToAssume);
        final var assumedRoles = roleRepo.fetchAssumedRoles();
        final var effectiveGroups = realSubjectRepo.findEffectiveSubjectGroups();

        // finally, return the result
        final var result = rbacContextResponse(
                currentSubjectUuid, currentSubject, claimedGroups, effectiveGroups, assumedRoles, isGlobalAdmin);
        return ResponseEntity.ok(result);
    }

    private RbacContextResource rbacContextResponse(
            final UUID currentSubjectUuid,
            final RealSubjectEntity currentSubject,
            final List<String> claimedGroups,
            final List<RealSubjectEntity> effectiveGroups,
            final List<RbacRoleEntity> assumedRoles,
            final boolean isGlobalAdmin) {
        final var result = new RbacContextResource();
        final var currentSubjectResource = new RbacSubjectResource();
        currentSubjectResource.setUuid(currentSubjectUuid);
        currentSubjectResource.setName(currentSubject.getName());
        currentSubjectResource.setType(SubjectTypeResource.valueOf(currentSubject.getType().name()));
        result.setSubject(currentSubjectResource);
        result.setClaimedGroups(claimedGroups);
        result.setEffectiveGroups(mapper.mapList(effectiveGroups, RbacSubjectGroupResource.class));
        result.setGlobalAdmin(isGlobalAdmin);
        final var assumedRolesResource = mapper.mapList(assumedRoles, RbacRoleResource.class);
        result.setAssumedRoles(assumedRolesResource);
        result.setApiKey(currentApiKeyProperties());
        return result;
    }

    /** The properties of the API-key used to authenticate the current request, from the synthesized JWT;
     * null if the request was authenticated via an OIDC JWT instead. */
    private static RbacContextApiKeyResource currentApiKeyProperties() {
        return ApiKeyAuthenticationFilter.currentApiKeyJwt().map(jwt -> {
            final var apiKeyResource = new RbacContextApiKeyResource();
            final var scopeClaim = jwt.getClaimAsString(ApiKeyAuthenticationFilter.SCOPE_CLAIM);
            apiKeyResource.setScopes(
                    scopeClaim == null || scopeClaim.isBlank() ? List.of() : Arrays.asList(scopeClaim.split(" ")));
            final var expiresAt = jwt.getClaimAsInstant(ApiKeyAuthenticationFilter.API_KEY_EXPIRES_AT_CLAIM);
            apiKeyResource.setExpiresAt(expiresAt == null ? null : expiresAt.atOffset(ZoneOffset.UTC));
            return apiKeyResource;
        }).orElse(null);
    }
}