RbacSubjectRepository.java

package net.hostsharing.hsadminng.rbac.subject;

import io.micrometer.core.annotation.Timed;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

import java.time.OffsetDateTime;
import java.util.List;
import java.util.UUID;

public interface RbacSubjectRepository extends Repository<RbacSubjectEntity, UUID> {

    // bypasses the restricted view, to be able to grant rights to arbitrary user
    @Query(value = "select * from rbac.subject where name=:userName", nativeQuery = true)
    @Timed("app.rbac.subjects.repo.findByName.rbac")
    RbacSubjectEntity findByName(String userName);

    @Timed("app.rbac.subjects.repo.findByUuid.rbac")
    RbacSubjectEntity findByUuid(UUID uuid);

    @Query(value = "select * from rbac.grantedPermissions(:subjectUuid)", nativeQuery = true)
    @Timed("app.rbac.subjects.repo.findPermissionsOfUserByUuid.rbac")
    List<RbacSubjectPermission> findPermissionsOfUserByUuid(UUID subjectUuid);

    /*
        Can't use save/saveAndFlush from SpringData because the uuid is not generated on the entity level,
        but explicitly, and then SpringData check's if it exists using an SQL SELECT.
        And SQL SELECT needs a currentSubject which we don't yet have in the case of self-registration.
     */
    @Modifying
    @Query(value = "insert into rbac.subject_rv (uuid, name, organization, type) values( :#{#newUser.uuid}, :#{#newUser.name}, :#{#newUser.organization}, cast(:#{#newUser.type.name()} as rbac.SubjectType))", nativeQuery = true)
    @Timed("app.rbac.subjects.repo.insert.rbac")
    void insert(final RbacSubjectEntity newUser);

    default RbacSubjectEntity create(final RbacSubjectEntity rbacSubjectEntity) {
        if (rbacSubjectEntity.getUuid() == null) {
            rbacSubjectEntity.setUuid(UUID.randomUUID());
        }
        if (rbacSubjectEntity.getType() == null) {
            rbacSubjectEntity.setType(SubjectType.USER);
        }
        insert(rbacSubjectEntity);
        // RbacSubjectEntity binds to 'rbac.subject_rv',
        // but the current user might not be allowed to read the newly created row from the restricted view,
        // only the newly created subject (or a global admin) is allowed to read the new subject.
        // Thus, the code which calls this method needs to switch the login user and fetch an attached entity.
        return rbacSubjectEntity; // Not yet attached to EM!
    }

    // idempotent upsert keyed by uuid; returns 'created' or 'updated'. The type is immutable.
    // A null organization is defaulted from the name prefix in the DB function.
    // deactivated=true deactivates the subject (soft-delete), false (re)activates it.
    @Query(value = "select rbac.upsert_subject(:uuid, :name, :organization, cast(:type as rbac.SubjectType), :deactivated)", nativeQuery = true)
    @Timed("app.rbac.subjects.repo.upsert.rbac")
    String upsert(UUID uuid, String name, String organization, String type, boolean deactivated);

    // stores the SHA-256 hash of the API-key of an API_KEY subject, its named
    // endpoint-scopes (empty array = unrestricted API-key), and its optional expiry
    // timestamp (null = never expires);
    // the DB function re-checks global-admin permission and the subject type
    @Query(
            value = "select rbac.create_api_key(:subjectUuid, :keyHash, cast(:scopes as varchar[]), cast(:expiresAt as timestamptz))",
            nativeQuery = true)
    @Timed("app.rbac.subjects.repo.createApiKey.rbac")
    UUID createApiKey(UUID subjectUuid, String keyHash, String[] scopes, OffsetDateTime expiresAt);

    // physical delete: removing the rbac.reference row cascades to the subject and, via the
    // rbac.subject delete triggers, to all of its grants and its account; a no-op if the uuid is
    // unknown. The subject's BEFORE DELETE trigger re-checks authorization at the DB level.
    @Modifying
    @Query(value = "delete from rbac.reference where uuid = :subjectUuid", nativeQuery = true)
    @Timed("app.rbac.subjects.repo.deleteByUuid.rbac")
    void deleteByUuid(UUID subjectUuid);
}