Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ensure same variable name is used in WHERE clause as in SELECT/FROM #124

Merged
merged 1 commit into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.contentgrid.spring.test.fixture.invoicing.InvoicingApplication;
import com.contentgrid.spring.test.fixture.invoicing.model.Customer;
import com.contentgrid.spring.test.fixture.invoicing.model.Invoice;
import com.contentgrid.spring.test.fixture.invoicing.model.Order;
Expand Down Expand Up @@ -43,16 +44,13 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.rest.webmvc.ContentGridSpringDataRestConfiguration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

@Transactional
@AutoConfigureMockMvc(printOnlyOnFailure = false)
@SpringBootTest(classes = com.contentgrid.spring.test.fixture.invoicing.InvoicingApplication.class)
@Import(ContentGridSpringDataRestConfiguration.class)
@SpringBootTest(classes = InvoicingApplication.class)
class ThunxDemoApplicationTests {

static final String INVOICE_1 = "I-2022-0001";
Expand Down Expand Up @@ -97,9 +95,15 @@ class ThunxDemoApplicationTests {

);

final Comparison POLICY_ORDERS_XENIT = Comparison.areEqual(
SymbolicReference.of("entity", path -> path.string("customer").string("vat")),
Scalar.of("BE0887582365")
);

static String PROMO_XMAS, PROMO_CYBER, PROMO_GORILLA;

UUID ORDER_1;
UUID ORDER_3;

@BeforeEach
void setupTestData() {
Expand All @@ -113,6 +117,7 @@ void setupTestData() {
ORDER_1 = orders.save(new Order(xenit)).getId();
var order2 = orders.save(new Order(xenit));
var order3 = orders.save(new Order(inbev));
ORDER_3 = order3.getId();

invoices.saveAll(List.of(
new Invoice(INVOICE_1, true, false, xenit,
Expand Down Expand Up @@ -723,38 +728,37 @@ class ToOne {

@Test
void deleteToOneAssoc_policyOk_shouldReturn_http204() throws Exception {
// policy can access all _UN_paid invoices
var policyUnpaidInvoices = Comparison.areEqual(
SymbolicReference.of("entity", path -> path.string("paid")),
Scalar.of(false));

// fictive example: dis-associate the customer from the invoice
// note that this would be classified as fraud in reality :grimacing:
mockMvc.perform(delete("/invoices/" + invoiceIdByNumber(INVOICE_1) + "/counterparty")
.header("X-ABAC-Context", headerEncode(policyUnpaidInvoices))
// policy can access all orders without an invoice
var ordersWithoutInvoice = Comparison.notEqual(
SymbolicReference.of("entity", path -> path.string("invoice").string("id")),
Scalar.nullValue()
);

// fictive example: dis-associate the customer from the order
mockMvc.perform(delete("/orders/" + ORDER_1 + "/customer")
.header("X-ABAC-Context", headerEncode(ordersWithoutInvoice))
.accept("application/json"))
.andExpect(status().isNoContent());
}


@Test
void deleteToOneAssoc_policyFail_preDelete_shouldReturn_http404() throws Exception {
// fictive example: dis-associate the customer from the invoice
// note that this would be classified as fraud in reality :grimacing:
// fictive example: dis-associate the customer from the order

// user does not have access to invoice-2
mockMvc.perform(delete("/invoices/" + invoiceIdByNumber(INVOICE_2) + "/counterparty")
.header("X-ABAC-Context", headerEncode(POLICY_INVOICES_XENIT))
// user does not have access to order-3
mockMvc.perform(delete("/orders/" + ORDER_3 + "/customer")
.header("X-ABAC-Context", headerEncode(POLICY_ORDERS_XENIT))
.accept("application/json"))
.andExpect(status().isNotFound());
}

@Test
void deleteToOneAssoc_policyFail_postDelete_shouldReturn_http404() throws Exception {
// fictive example: dis-associate the customer from the invoice
// user has access to invoice-1 - as long as customer = xenit
mockMvc.perform(delete("/invoices/" + invoiceIdByNumber(INVOICE_1) + "/counterparty")
.header("X-ABAC-Context", headerEncode(POLICY_INVOICES_XENIT))
// fictive example: dis-associate the customer from the order
// user has access to order-1 - as long as customer = xenit
mockMvc.perform(delete("/orders/" + ORDER_1 + "/customer")
.header("X-ABAC-Context", headerEncode(POLICY_ORDERS_XENIT))
.accept("application/json"))
.andExpect(status().isNotFound());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.contentgrid.thunx.predicates.querydsl;

import com.querydsl.core.types.dsl.PathBuilder;

public interface PathBuilderFactory {
PathBuilder<?> create(Class<?> domainType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,21 @@
public class QueryDslConverter {

private final QueryDslConvertingVisitor visitor;
private final PathBuilderFactory pathBuilderFactory;

public QueryDslConverter(PropertyAccessStrategy propertyAccessStrategy) {
public QueryDslConverter(PropertyAccessStrategy propertyAccessStrategy, PathBuilderFactory pathBuilderFactory) {
this.visitor = new QueryDslConvertingVisitor(propertyAccessStrategy);
this.pathBuilderFactory = pathBuilderFactory;
}

public Predicate from(ThunkExpression<Boolean> thunk, Class<?> domainType) {
var entityPath = new PathBuilder<>(domainType, toAlias(domainType));
var entityPath = this.pathBuilderFactory.create(domainType);
return this.from(thunk, entityPath);
}

public Predicate from(ThunkExpression<Boolean> thunk, PathBuilder<?> entityPath) {
return (Predicate) thunk.accept(this.visitor, new QueryDslConversionContext(entityPath));
}


private static String toAlias(Class<?> domainType) {
return uncapitalize(domainType.getSimpleName());
}

private static String uncapitalize(String str) {
if (str == null || str.isEmpty()) {
return str;
}

char baseChar = str.charAt(0);
char updatedChar;
updatedChar = Character.toLowerCase(baseChar);

if (baseChar == updatedChar) {
return str;
}

char[] chars = str.toCharArray();
chars[0] = updatedChar;
return new String(chars);
}


}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import com.contentgrid.thunx.predicates.model.LogicalOperation;
import com.contentgrid.thunx.predicates.model.Scalar;
import com.contentgrid.thunx.predicates.model.SymbolicReference;
import com.querydsl.core.types.dsl.PathBuilder;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
import javax.persistence.Embeddable;
import javax.persistence.Embedded;
Expand All @@ -18,7 +20,8 @@

class QueryDslConverterTest {

private final QueryDslConverter converter = new QueryDslConverter(new FieldByReflectionAccessStrategy());
private final QueryDslConverter converter = new QueryDslConverter(new FieldByReflectionAccessStrategy(), domainType -> new PathBuilder<>(domainType, domainType.getSimpleName().toLowerCase(
Locale.ROOT)));

static class Document {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.contentgrid.thunx.spring.data.querydsl;

import com.contentgrid.thunx.predicates.querydsl.PathBuilderFactory;
import com.querydsl.core.types.dsl.PathBuilder;
import org.springframework.data.querydsl.EntityPathResolver;

/**
* Creates a {@link PathBuilder<?>} from a domain type by using the metadata provided by {@link EntityPathResolver}
*/
public class EntityPathResolverBasedPathBuilderFactory implements PathBuilderFactory {

private final EntityPathResolver resolver;

public EntityPathResolverBasedPathBuilderFactory(EntityPathResolver resolver) {
this.resolver = resolver;
}

@Override
public PathBuilder<?> create(Class<?> domainType) {
var entityPath = resolver.createPath(domainType);
return new PathBuilder<>(domainType, entityPath.getMetadata());
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.contentgrid.thunx.spring.data.rest;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.contentgrid.thunx.encoding.ThunkExpressionDecoder;
import com.contentgrid.thunx.predicates.model.ThunkExpression;
import com.contentgrid.thunx.encoding.json.ExpressionJsonConverter;
import com.contentgrid.thunx.predicates.model.ThunkExpression;
import com.fasterxml.jackson.core.JsonProcessingException;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;

import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
Expand Down Expand Up @@ -78,10 +77,11 @@ public Object postProcessAfterInitialization(Object bean, String beanName) throw
var resourceMetadataResolver = applicationContext.getBean(ResourceMetadataHandlerMethodArgumentResolver.class);
var factory = applicationContext.getBean(QuerydslBindingsFactory.class);
var transactionManager = applicationContext.getBean(PlatformTransactionManager.class);
var entityPathResolver = factory.getEntityPathResolver();

var defaultConversionService = new DefaultFormattingConversionService(); // ??
var predicateBuilder = new AbacQuerydslPredicateBuilder(defaultConversionService, factory.getEntityPathResolver());
var repositoryInvokerFactory = new AbacRepositoryInvokerAdapterFactory(repositories, transactionManager);
var predicateBuilder = new AbacQuerydslPredicateBuilder(defaultConversionService, entityPathResolver);
var repositoryInvokerFactory = new AbacRepositoryInvokerAdapterFactory(repositories, transactionManager, entityPathResolver);


return new AbacRootResourceInformationHandlerMethodArgumentResolver(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.dsl.Expressions;
import com.querydsl.core.types.dsl.PathBuilderFactory;
import com.querydsl.core.types.dsl.PathBuilder;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
Expand All @@ -30,9 +30,6 @@ public class AbacRepositoryInvokerAdapter extends QuerydslRepositoryInvokerAdapt
private final Predicate predicate;
private final PlatformTransactionManager transactionManager;

@NonNull
private final Class<?> domainType;

@NonNull
private final Class<?> idPropertyType;

Expand All @@ -42,6 +39,9 @@ public class AbacRepositoryInvokerAdapter extends QuerydslRepositoryInvokerAdapt
@NonNull
private final Function<Object, Optional<?>> idFunction;

@NonNull
private final PathBuilder<?> pathBuilder;

private final ConversionService conversionService = new DefaultFormattingConversionService();

public AbacRepositoryInvokerAdapter(
Expand All @@ -51,30 +51,34 @@ public AbacRepositoryInvokerAdapter(
PlatformTransactionManager transactionManager,
RepositoryMetadata repositoryMetadata,
PersistentEntity<?, ?> persistentEntity,
EntityInformation<Object, ?> entityInformation) {
this(delegate, executor, predicate, transactionManager, repositoryMetadata.getDomainType(),
repositoryMetadata.getIdType(), persistentEntity.getRequiredIdProperty().getName(),
entity -> Optional.ofNullable(entityInformation.getId(entity)));
EntityInformation<Object, ?> entityInformation,
PathBuilder<?> pathBuilder
) {
this(delegate, executor, predicate, transactionManager, repositoryMetadata.getIdType(),
persistentEntity.getRequiredIdProperty().getName(),
entity -> Optional.ofNullable(entityInformation.getId(entity)),
pathBuilder
);
}

public AbacRepositoryInvokerAdapter(
RepositoryInvoker delegate,
QuerydslPredicateExecutor<Object> executor,
Predicate predicate,
PlatformTransactionManager transactionManager,
Class<?> domainType,
Class<?> idType,
String idPropertyName,
Function<Object, Optional<?>> idFunction
Function<Object, Optional<?>> idFunction,
PathBuilder<?> pathBuilder
) {
super(delegate, executor, predicate);
this.executor = executor;
this.predicate = predicate;
this.transactionManager = transactionManager;
this.domainType = domainType;
this.idPropertyType = idType;
this.idName = idPropertyName;
this.idFunction = idFunction;
this.pathBuilder = pathBuilder;

}

Expand All @@ -91,7 +95,6 @@ public <T> Optional<T> invokeFindById(Object id) {
BooleanBuilder builder = new BooleanBuilder();
builder.and(predicate);

var pathBuilder = new PathBuilderFactory().create(this.domainType);
var entityIdPath = pathBuilder.get(this.idName, this.idPropertyType);
Assert.notNull(entityIdPath, "id expression cannot be null");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.contentgrid.thunx.spring.data.rest;

import com.contentgrid.thunx.predicates.querydsl.PathBuilderFactory;
import com.contentgrid.thunx.spring.data.querydsl.EntityPathResolverBasedPathBuilderFactory;
import com.querydsl.core.types.Predicate;
import lombok.AllArgsConstructor;
import org.springframework.data.querydsl.EntityPathResolver;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.data.repository.support.Repositories;
import org.springframework.data.repository.support.RepositoryInvoker;
Expand All @@ -10,8 +13,13 @@
@AllArgsConstructor
public class AbacRepositoryInvokerAdapterFactory {

private Repositories repositories;
private PlatformTransactionManager transactionManager;
private final Repositories repositories;
private final PlatformTransactionManager transactionManager;
private final PathBuilderFactory pathBuilderFactory;

public AbacRepositoryInvokerAdapterFactory(Repositories repositories, PlatformTransactionManager transactionManager, EntityPathResolver entityPathResolver) {
this(repositories, transactionManager, new EntityPathResolverBasedPathBuilderFactory(entityPathResolver));
}

public RepositoryInvoker createRepositoryInvoker(RepositoryInvoker repositoryInvoker, Class<?> domainType,
Predicate predicate) {
Expand All @@ -24,7 +32,7 @@ public RepositoryInvoker createRepositoryInvoker(RepositoryInvoker repositoryInv
var entityInformation = repositories.getEntityInformationFor(domainType);

return new AbacRepositoryInvokerAdapter(repositoryInvoker, executor, predicate, transactionManager,
repositoryInformation, persistentEntity, entityInformation);
repositoryInformation, persistentEntity, entityInformation, pathBuilderFactory.create(domainType));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.contentgrid.thunx.predicates.querydsl.FieldByReflectionAccessStrategy;
import com.contentgrid.thunx.predicates.querydsl.QueryDslConverter;
import com.contentgrid.thunx.spring.data.context.AbacContext;
import com.contentgrid.thunx.spring.data.querydsl.EntityPathResolverBasedPathBuilderFactory;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
Expand Down Expand Up @@ -45,7 +46,8 @@ public AbacQuerydslPredicateBuilder(ConversionService conversionService, EntityP
this.paths = new ConcurrentHashMap<>();
this.resolver = resolver;

this.queryDslConverter = new QueryDslConverter(new FieldByReflectionAccessStrategy());
this.queryDslConverter = new QueryDslConverter(new FieldByReflectionAccessStrategy(), new EntityPathResolverBasedPathBuilderFactory(
resolver));
}

@Nullable
Expand Down Expand Up @@ -91,13 +93,6 @@ public Predicate getPredicate(TypeInformation<?> type, MultiValueMap<String, Str
return builder.getValue();
}

private String toAlias(Class<?> subjectType) {

char c[] = subjectType.getSimpleName().toCharArray();
c[0] = Character.toLowerCase(c[0]);
return new String(c);
}

/**
* Invokes the binding of the given values, for the given {@link PropertyPath} and {@link QuerydslBindings}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ class AbacRepositoryInvokerAdapterTest {
@BeforeEach
void setUp() {
this.adapter = new AbacRepositoryInvokerAdapter(delegate, executor, predicate, transactionManager,
MyEntity.class, UUID.class, "id", (entity) -> Optional.ofNullable(((MyEntity) entity).getId()));
UUID.class, "id", (entity) -> Optional.ofNullable(((MyEntity) entity).getId()),
new PathBuilder<>(MyEntity.class, "myEntity")
);
}

@Test
Expand Down