Skip to content

Commit

Permalink
Split off implementation that allows supplying arbitrary QueryDSL pre…
Browse files Browse the repository at this point in the history
…dicates

This is a focussed, reusable piece of code that is not tightly bound to the Thunx ABAC system, and that is reusable for other purposes without having to use all of thunx.
  • Loading branch information
vierbergenlars committed Aug 29, 2023
1 parent e2735b2 commit e4b0179
Show file tree
Hide file tree
Showing 28 changed files with 812 additions and 184 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ that fulfill the conditional authorization predicate.
![overview](./resources/diagrams/container-diagram-overview.png)

[C4 model]: https://c4model.com/
[QueryDSL support in Spring Data]: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl]

[Spring Data QueryDSL Extension]: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.extensions.querydsl]

### Solution mechanics

Expand All @@ -86,32 +87,33 @@ This repository has several modules:
* `thunx-encoding-json` is a JSON-serialization library for thunk-expressions
* `thunx-predicates-querydsl` is a library to convert thunk-expressions into QueryDSL predicates
* `thunx-spring` provides an integration with Spring Cloud Gateway and Spring Data REST
* `thunx-spring-querydsl-predicate-resolver` is a library to inject additional [QueryDSL] predicates to be processed
together with the predicates from the [Spring Data QueryDSL Extension]

## Getting Started

### Installation

Requirements:
* Java 11+

* Java 17+

#### Spring Cloud Gateway

Using Gradle:

```groovy
implementation "com.contentgrid.thunx:thunx-spring:${thunxVersion}"
implementation "com.contentgrid.thunx:thunx-pdp-opa:${thunxVersion}"
implementation "com.contentgrid.thunx:thunx-gateway-spring-boot-starter:${thunxVersion}"
```

#### Spring Data REST Service

Using Gradle:

```groovy
implementation "com.contentgrid.thunx:thunx-spring:${thunxVersion}"
runtimeOnly "com.contentgrid.thunx:thunx-predicates-querydsl:${thunxVersion}"
implementation "com.contentgrid.thunx:thunx-gateway-spring-boot-starter:${thunxVersion}"
implementation "com.querydsl:querydsl-core"
implementation "com.querydsl:querydsl-jpa"
implementation "com.querydsl:querydsl-jpa" // Or another QueryDSL implementation, dependent on the spring data flavor you're using
```


Expand Down
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ allprojects {
canBeConsumed = false
canBeResolved = false
}
annotationProcessor.extendsFrom(internalPlatform)
compileClasspath.extendsFrom(internalPlatform)
runtimeClasspath.extendsFrom(internalPlatform)
testAnnotationProcessor.extendsFrom(internalPlatform)
testCompileClasspath.extendsFrom(internalPlatform)
testRuntimeClasspath.extendsFrom(internalPlatform)
}
Expand Down
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ include 'thunx-model'
include 'thunx-pdp'
include 'thunx-pdp-opa'
include 'thunx-spring'
include 'spring-data-querydsl-predicate-injector'
include 'thunx-autoconfigure'
include 'thunx-api-spring-boot-starter'
include 'thunx-gateway-spring-boot-starter'
Expand Down
44 changes: 44 additions & 0 deletions spring-data-querydsl-predicate-injector/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
plugins {
id 'java-library'
id 'maven-publish'
}

dependencies {
internalPlatform platform(project(':thunx-dependencies'))

compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

api 'com.querydsl:querydsl-core'
api 'org.springframework:spring-core'

implementation 'org.springframework.data:spring-data-rest-webmvc'

compileOnly("org.springframework.boot:spring-boot-autoconfigure") {
because 'used for autoconfiguration annotations'
}

testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'

testAnnotationProcessor 'jakarta.persistence:jakarta.persistence-api'
testAnnotationProcessor 'com.querydsl:querydsl-apt::jakarta'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.springframework.boot:spring-boot-starter-data-rest'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa'
testImplementation 'com.querydsl:querydsl-jpa::jakarta'

testImplementation "org.assertj:assertj-core"
testImplementation "org.junit.jupiter:junit-jupiter-api"

testImplementation 'org.testcontainers:testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:postgresql'
testRuntimeOnly 'org.postgresql:postgresql'
}

test {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions spring-data-querydsl-predicate-injector/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
description=Inject additional QueryDSL predicates in Spring Data REST repositories
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.repository;

import com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver.OperationPredicates;
import com.querydsl.core.types.Predicate;
import org.springframework.data.repository.support.RepositoryInvoker;

/**
* Adapts a {@link RepositoryInvoker} with one applies the {@link Predicate} to results
*/
@FunctionalInterface
public interface RepositoryInvokerAdapterFactory {

/**
* Adapt a repository invoker to apply the supplied QueryDSL predicate
*
* @param repositoryInvoker Original invoker
* @param domainType Domain class for which the invoker is requested
* @param predicate Predicate to apply to the invoker
* @return An invoker that applies the predicate
*/
RepositoryInvoker adaptRepositoryInvoker(RepositoryInvoker repositoryInvoker, Class<?> domainType, OperationPredicates predicate);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver;

import com.contentgrid.thunx.spring.data.querydsl.predicate.injector.repository.RepositoryInvokerAdapterFactory;
import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import lombok.RequiredArgsConstructor;

/**
* Only applies predicates on the collection of entities
* <p>
* This implementation of {@link OperationPredicates} is the only implementation that can be used with the out-of-the
* box {@link RepositoryInvokerAdapterFactory}
*/
@RequiredArgsConstructor
public class CollectionFilteringOnlyOperationPredicates implements OperationPredicates {

private final Predicate predicate;

@Override
public OperationPredicates and(OperationPredicates predicate) {
if (predicate instanceof CollectionFilteringOnlyOperationPredicates) {
return new CollectionFilteringOnlyOperationPredicates(ExpressionUtils.and(
this.readPredicate(),
predicate.readPredicate()
));
}
return OperationPredicates.super.and(predicate);
}

@Override
public Predicate readPredicate() {
return predicate;
}

@Override
public Predicate afterCreatePredicate() {
return null;
}

@Override
public Predicate beforeUpdatePredicate() {
return null;
}

@Override
public Predicate afterUpdatePredicate() {
return null;
}

@Override
public Predicate beforeDeletePredicate() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver;

import com.querydsl.core.types.ExpressionUtils;
import com.querydsl.core.types.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.Nullable;

@RequiredArgsConstructor
class CompositeOperationPredicates implements OperationPredicates {
private final List<OperationPredicates> predicates;

@Override
public OperationPredicates and(OperationPredicates predicate) {
var copy = new ArrayList<>(predicates);
copy.add(predicate);
return new CompositeOperationPredicates(copy);
}

@Nullable
private Predicate combine(Function<OperationPredicates, Predicate> extractor) {
return predicates.stream()
.map(extractor)
.reduce(ExpressionUtils::and)
.orElse(null);
}

@Override
public Predicate readPredicate() {
return combine(OperationPredicates::readPredicate);
}

@Override
public Predicate afterCreatePredicate() {
return combine(OperationPredicates::afterCreatePredicate);
}

@Override
public Predicate beforeUpdatePredicate() {
return combine(OperationPredicates::beforeUpdatePredicate);
}

@Override
public Predicate afterUpdatePredicate() {
return combine(OperationPredicates::afterUpdatePredicate);
}

@Override
public Predicate beforeDeletePredicate() {
return combine(OperationPredicates::beforeDeletePredicate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver;

import com.querydsl.core.types.Predicate;
import java.util.List;
import org.springframework.lang.Nullable;

/**
* A set of QueryDSL predicates that apply to different operations that can be executed on an entity
*/
public interface OperationPredicates {

/**
* Combines this predicate with a different predicate
*
* @param predicate The predicate to AND together with
* @return The combined predicate
*/
default OperationPredicates and(OperationPredicates predicate) {
return new CompositeOperationPredicates(List.of(this, predicate));
}

/**
* @return Predicate used for reading an entity
*/
@Nullable
Predicate readPredicate();

/**
* @return Predicate used to check permissions for creating an entity with certain values
*/
@Nullable
Predicate afterCreatePredicate();

/**
* @return Predicate used to check permission before updating an entity
*/
@Nullable
Predicate beforeUpdatePredicate();

/**
* @return Predicate used to check permissions for updating an entity with certain values
*/
@Nullable
Predicate afterUpdatePredicate();

/**
* @return Predicate used for deleting an entity
*/
@Nullable
Predicate beforeDeletePredicate();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.contentgrid.thunx.spring.data.querydsl;
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver;

import com.querydsl.core.types.Predicate;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.MethodParameter;

/**
* Resolves a QueryDSL {@link Predicate} that will be applied to requests by the
* {@link org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver}
* Resolves a QueryDSL {@link Predicate} for injection into a method parameter of a Spring controller
*/
public interface QuerydslPredicateResolver {

Expand All @@ -19,5 +18,5 @@ public interface QuerydslPredicateResolver {
* @param parameters All parameters parsed from the requests's query string
* @return The predicate to apply to the request
*/
Optional<Predicate> resolve(MethodParameter methodParameter, Class<?> domainType, Map<String, String[]> parameters);
Optional<OperationPredicates> resolve(MethodParameter methodParameter, Class<?> domainType, Map<String, String[]> parameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.contentgrid.thunx.spring.data.querydsl.predicate.injector.rest.webmvc;

import com.contentgrid.thunx.spring.data.querydsl.predicate.injector.repository.RepositoryInvokerAdapterFactory;
import com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver.QuerydslPredicateResolver;
import com.contentgrid.thunx.spring.data.querydsl.predicate.injector.resolver.OperationPredicates;
import java.util.Map;
import java.util.Optional;
import lombok.NonNull;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.core.MethodParameter;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvoker;
import org.springframework.data.repository.support.RepositoryInvokerFactory;
import org.springframework.data.rest.webmvc.RootResourceInformation;
import org.springframework.data.rest.webmvc.config.ResourceMetadataHandlerMethodArgumentResolver;
import org.springframework.data.rest.webmvc.config.RootResourceInformationHandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;

/**
* {@link HandlerMethodArgumentResolver} to create {@link RootResourceInformation} for injection into Spring MVC
* controller methods.
* <p>
* This variant injects a custom {@link RepositoryInvoker} that filters its output based on QueryDSL predicates resolved
* by {@link QuerydslPredicateResolver}
*/
class PredicateInjectingRootResourceInformationHandlerMethodArgumentResolver extends
RootResourceInformationHandlerMethodArgumentResolver {

private final RepositoryInvokerAdapterFactory repositoryInvokerAdapterFactory;
private final ObjectProvider<QuerydslPredicateResolver> predicateResolvers;

public PredicateInjectingRootResourceInformationHandlerMethodArgumentResolver(
Repositories repositories,
RepositoryInvokerFactory invokerFactory,
ResourceMetadataHandlerMethodArgumentResolver resourceMetadataResolver,
@NonNull RepositoryInvokerAdapterFactory repositoryInvokerAdapterFactory,
@NonNull ObjectProvider<QuerydslPredicateResolver> predicateResolvers
) {
super(repositories, invokerFactory, resourceMetadataResolver);

this.repositoryInvokerAdapterFactory = repositoryInvokerAdapterFactory;
this.predicateResolvers = predicateResolvers;
}

@Override
protected RepositoryInvoker postProcess(MethodParameter parameter, RepositoryInvoker invoker, Class<?> domainType, Map<String, String[]> parameters) {
return getPredicate(parameter, domainType, parameters)
.map(predicate -> repositoryInvokerAdapterFactory.adaptRepositoryInvoker(invoker, domainType, predicate))
.orElse(invoker);
}

private Optional<OperationPredicates> getPredicate(MethodParameter parameter, Class<?> domainType, Map<String, String[]> parameters) {
return predicateResolvers.stream()
.map(resolver -> resolver.resolve(parameter, domainType, parameters))
.flatMap(Optional::stream)
.reduce(OperationPredicates::and);
}
}
Loading

0 comments on commit e4b0179

Please sign in to comment.