Query Services

Overview

Query services provide security-filtered data access using the SecuritySpecifications helper class. Security filtering is implemented in extended (*Ex) classes to preserve JHipster code generation compatibility.

Architecture

Design Principles

  1. Helper class over inheritance - Use SecuritySpecifications helper methods instead of base class inheritance

  2. Type-safe paths - Use JPA metamodel functions for compile-time path validation

  3. Extended classes - Security methods go in *QueryServiceEx classes, not generated classes

  4. Selective filtering - Apply only the security dimensions each entity needs

JHipster Compatibility

JHipster regenerates <Entity>QueryService classes. To preserve custom security code:

  • Generated classes extend JHipster’s QueryService<ENTITY>

  • Custom security methods go in <Entity>QueryServiceEx extends <Entity>QueryService

  • REST controllers inject and use the *Ex class for secured operations

SecuritySpecifications Helper

The SecuritySpecifications class provides static methods to apply security filters to JPA specifications.

Use function-based methods for type-safe path resolution:

// Apply organisation filter with type-safe path
public static <T> Specification<T> applyOrgFilter(
    Specification<T> baseSpec,
    SecurityDimensionService securityService,
    AccessLevel accessLevel,
    Function<Root<T>, Expression<Long>> orgIdPath)

// Apply person filter with type-safe path
public static <T> Specification<T> applyPersonFilter(
    Specification<T> baseSpec,
    SecurityDimensionService securityService,
    Function<Root<T>, Expression<Long>> personIdPath)

For entities requiring both organisation AND person filtering, simply chain the methods:

Specification<EventParticipant> spec = createSpecification(criteria);
spec = SecuritySpecifications.applyOrgFilter(spec, securityService, AccessLevel.READ, ORG_PATH);
spec = SecuritySpecifications.applyPersonFilter(spec, securityService, PERSON_PATH);

Path Functions

Define path functions using JPA static metamodels:

// Direct relationship: Event -> organiser -> id
Function<Root<Event>, Expression<Long>> eventOrgPath =
    root -> root.join(Event_.organiser, JoinType.LEFT).get(Organisation_.id);

// Transitive relationship: EventParticipant -> event -> organiser -> id
Function<Root<EventParticipant>, Expression<Long>> participantOrgPath =
    root -> root.join(EventParticipant_.event, JoinType.LEFT)
               .join(Event_.organiser, JoinType.LEFT)
               .get(Organisation_.id);

// Deep transitive: Race -> eventRaceType -> event -> organiser -> id
Function<Root<Race>, Expression<Long>> raceOrgPath =
    root -> root.join(Race_.eventRaceType, JoinType.LEFT)
               .join(EventRaceType_.event, JoinType.LEFT)
               .join(Event_.organiser, JoinType.LEFT)
               .get(Organisation_.id);

Implementation Pattern

Organisation-Scoped Entity (Event)

@Service
@Transactional(readOnly = true)
public class EventQueryServiceEx extends EventQueryService {

    @Autowired
    private SecurityDimensionService securityDimensionService;

    @Autowired
    private EventRepository eventRepository;

    @Autowired
    private EventMapper eventMapper;

    // Define org path as constant for reuse
    private static final Function<Root<Event>, Expression<Long>> ORG_PATH =
        root -> root.join(Event_.organiser, JoinType.LEFT).get(Organisation_.id);

    /**
     * Find events with security filtering applied.
     * Only returns events from organisations the user has access to.
     */
    public Page<EventDTO> findByCriteriaSecure(EventCriteria criteria, Pageable page) {
        Specification<Event> spec = createSpecification(criteria);
        spec = SecuritySpecifications.applyOrgFilter(
            spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
        return eventRepository.findAll(spec, page).map(eventMapper::toDto);
    }

    /**
     * Find single event with security filtering.
     */
    public Optional<EventDTO> findOneSecure(Long id) {
        Specification<Event> spec = (root, query, cb) -> cb.equal(root.get(Event_.id), id);
        spec = SecuritySpecifications.applyOrgFilter(
            spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
        return eventRepository.findOne(spec).map(eventMapper::toDto);
    }
}

Composite-Scoped Entity (EventParticipant)

For entities requiring both organisation AND person access:

@Service
@Transactional(readOnly = true)
public class EventParticipantQueryServiceEx extends EventParticipantQueryService {

    @Autowired
    private SecurityDimensionService securityDimensionService;

    // Org path: EventParticipant -> event -> organiser -> id
    private static final Function<Root<EventParticipant>, Expression<Long>> ORG_PATH =
        root -> root.join(EventParticipant_.event, JoinType.LEFT)
                   .join(Event_.organiser, JoinType.LEFT)
                   .get(Organisation_.id);

    // Person path: EventParticipant -> person -> id
    private static final Function<Root<EventParticipant>, Expression<Long>> PERSON_PATH =
        root -> root.join(EventParticipant_.person, JoinType.LEFT).get(Person_.id);

    /**
     * Find participants with composite security filtering.
     * User must have access to BOTH the organisation AND the person.
     */
    public Page<EventParticipantDTO> findByCriteriaSecure(
            EventParticipantCriteria criteria, Pageable page) {
        Specification<EventParticipant> spec = createSpecification(criteria);
        spec = SecuritySpecifications.applyOrgFilter(
            spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
        spec = SecuritySpecifications.applyPersonFilter(spec, securityDimensionService, PERSON_PATH);
        return eventParticipantRepository.findAll(spec, page)
            .map(eventParticipantMapper::toDto);
    }
}

Transitive Path Entity (Race)

For entities with deep relationship chains:

@Service
@Transactional(readOnly = true)
public class RaceQueryServiceEx extends RaceQueryService {

    @Autowired
    private SecurityDimensionService securityDimensionService;

    // Deep path: Race -> eventRaceType -> event -> organiser -> id
    private static final Function<Root<Race>, Expression<Long>> ORG_PATH =
        root -> root.join(Race_.eventRaceType, JoinType.LEFT)
                   .join(EventRaceType_.event, JoinType.LEFT)
                   .join(Event_.organiser, JoinType.LEFT)
                   .get(Organisation_.id);

    public Page<RaceDTO> findByCriteriaSecure(RaceCriteria criteria, Pageable page) {
        Specification<Race> spec = createSpecification(criteria);
        spec = SecuritySpecifications.applyOrgFilter(
            spec, securityDimensionService, AccessLevel.READ, ORG_PATH);
        return raceRepository.findAll(spec, page).map(raceMapper::toDto);
    }
}

Entity Path Reference

Common entity paths to organisation/person:

Entity Organisation Path Person Path

Event

organiser.id

N/A

EventParticipant

event.organiser.id

person.id

Race

eventRaceType.event.organiser.id

N/A

EventCategory

event.organiser.id

N/A

Membership

organisation.id

person.id

REST Controller Integration

Controllers should inject and use the *Ex class:

@RestController
@RequestMapping("/api/events")
public class EventResource {

    private final EventQueryServiceEx eventQueryServiceEx;  // Use Ex class

    @GetMapping
    public ResponseEntity<List<EventDTO>> getAllEvents(EventCriteria criteria, Pageable page) {
        // Use secured method
        Page<EventDTO> result = eventQueryServiceEx.findByCriteriaSecure(criteria, page);
        return ResponseEntity.ok()
            .headers(PaginationUtil.generatePaginationHttpHeaders(result, "/api/events"))
            .body(result.getContent());
    }

    @GetMapping("/{id}")
    public ResponseEntity<EventDTO> getEvent(@PathVariable Long id) {
        return eventQueryServiceEx.findOneSecure(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
}

Deprecated: SecuredQueryService Base Class

The SecuredQueryService<ENTITY> base class is deprecated. Use SecuritySpecifications helper methods instead.

The base class approach had several drawbacks:

  • Forced all security dimensions on derived classes

  • Used try-catch for path resolution (error-prone)

  • Conflicted with JHipster code generation

Next Steps