Add support for plugable QL for EntityManager (#2698)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -31,7 +31,6 @@ import java.util.Objects;
|
||||
import java.util.function.BiPredicate;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison.Operator;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
||||
|
||||
/**
|
||||
* Provides entity matcher that matches an entity object against a filter (a {@link Node} or an RSQL string).
|
||||
@@ -44,14 +43,10 @@ public class EntityMatcher {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public static EntityMatcher forNode(final Node root) {
|
||||
public static EntityMatcher of(final Node root) {
|
||||
return new EntityMatcher(root);
|
||||
}
|
||||
|
||||
public static EntityMatcher forRsql(final String rsql) {
|
||||
return forNode(RsqlParser.parse(rsql));
|
||||
}
|
||||
|
||||
public <T> boolean match(final T t) {
|
||||
return match(t, root);
|
||||
}
|
||||
|
||||
@@ -149,8 +149,7 @@ public class QLSupport {
|
||||
* given {@code fieldNameProvider}
|
||||
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
|
||||
*/
|
||||
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(
|
||||
final String query, final Class<A> queryFieldType) {
|
||||
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final String query, final Class<A> queryFieldType) {
|
||||
if (specBuilder == SpecBuilder.G3) {
|
||||
return new SpecificationBuilder<T>(!caseInsensitiveDB && ignoreCase, database)
|
||||
.specification(parser.parse(caseInsensitiveDB || ignoreCase ? query.toLowerCase() : query, queryFieldType));
|
||||
@@ -159,6 +158,10 @@ public class QLSupport {
|
||||
}
|
||||
}
|
||||
|
||||
public <A extends Enum<A> & QueryField> EntityMatcher entityMatcher(final String query, final Class<A> queryFieldType) {
|
||||
return EntityMatcher.of(parser.parse(caseInsensitiveDB || ignoreCase ? query.toLowerCase() : query, queryFieldType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the query string
|
||||
*
|
||||
@@ -168,7 +171,7 @@ public class QLSupport {
|
||||
* @throws QueryException if query is invalid
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public <A extends Enum<A> & QueryField> void validateQuery(final String query, final Class<A> queryFieldType, final Class<?> jpaType) {
|
||||
public <A extends Enum<A> & QueryField> void validate(final String query, final Class<A> queryFieldType, final Class<?> jpaType) {
|
||||
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
|
||||
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
|
||||
buildSpec(query, queryFieldType).toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
|
||||
|
||||
@@ -50,14 +50,14 @@ public class SpecificationBuilderLegacy<A extends Enum<A> & QueryField, T> {
|
||||
|
||||
public Specification<T> specification(final String rsql) {
|
||||
return (root, query, cb) -> {
|
||||
final QLSupport rsqlUtility = QLSupport.getInstance();
|
||||
final QLSupport qlSupport = QLSupport.getInstance();
|
||||
|
||||
final Node rootNode = parseRsql(rsql, rsqlUtility);
|
||||
final Node rootNode = parseRsql(rsql, qlSupport);
|
||||
query.distinct(true);
|
||||
|
||||
final boolean ensureIgnoreCase = !rsqlUtility.isCaseInsensitiveDB() && rsqlUtility.isIgnoreCase();
|
||||
final boolean ensureIgnoreCase = !qlSupport.isCaseInsensitiveDB() && qlSupport.isIgnoreCase();
|
||||
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
|
||||
rsqlUtility.getSpecBuilder() == LEGACY_G1
|
||||
qlSupport.getSpecBuilder() == LEGACY_G1
|
||||
? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyReplacer, database, query, ensureIgnoreCase)
|
||||
: new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, database, virtualPropertyReplacer, ensureIgnoreCase);
|
||||
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
||||
|
||||
@@ -553,7 +553,7 @@ class SpecificationBuilderTest {
|
||||
|
||||
private List<Root> filter(final String rsql) {
|
||||
// reference / auto filter (using elements and reflection)
|
||||
final EntityMatcher matcher = EntityMatcher.forRsql(rsql);
|
||||
final EntityMatcher matcher = EntityMatcher.of(RsqlParser.parse(rsql));
|
||||
final List<Root> refResult = StreamSupport.stream(rootRepository.findAll().spliterator(), false).filter(matcher::match).toList();
|
||||
final List<Root> result = rootRepository.findAll(getSpecification(rsql));
|
||||
// auto check with reference result
|
||||
|
||||
@@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.ContextAware;
|
||||
import org.eclipse.hawkbit.repository.QueryField;
|
||||
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@@ -34,17 +33,17 @@ import org.springframework.util.ObjectUtils;
|
||||
@Slf4j
|
||||
public class DefaultAccessController<A extends Enum<A> & QueryField, T> implements AccessController<T> {
|
||||
|
||||
private final Class<A> rsqlQueryFieldType;
|
||||
private final Class<A> queryFieldType;
|
||||
private final Map<Operation, List<String>> permissions = new EnumMap<>(Operation.class);
|
||||
|
||||
private ContextAware contextAware;
|
||||
|
||||
public DefaultAccessController(final Class<A> rsqlQueryFieldType, final String... permissionTypes) {
|
||||
public DefaultAccessController(final Class<A> queryFieldType, final String... permissionTypes) {
|
||||
if (ObjectUtils.isEmpty(permissionTypes)) {
|
||||
throw new IllegalArgumentException("Permission types must not be empty");
|
||||
}
|
||||
|
||||
this.rsqlQueryFieldType = rsqlQueryFieldType;
|
||||
this.queryFieldType = queryFieldType;
|
||||
for (final Operation operation : Operation.values()) {
|
||||
for (final String permissionType : permissionTypes) {
|
||||
permissions.computeIfAbsent(operation, k -> new ArrayList<>()).add(operation.name() + "_" + permissionType.toUpperCase());
|
||||
@@ -69,7 +68,7 @@ public class DefaultAccessController<A extends Enum<A> & QueryField, T> implemen
|
||||
scopes.size() == 1
|
||||
? scopes.get(0) // single scope
|
||||
: "(" + String.join(") or (", scopes) + ")") // join multiple scopes with 'or' - union
|
||||
.map(rsql -> QLSupport.getInstance().buildSpec(rsql, rsqlQueryFieldType));
|
||||
.map(rsql -> QLSupport.getInstance().buildSpec(rsql, queryFieldType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,7 +81,7 @@ public class DefaultAccessController<A extends Enum<A> & QueryField, T> implemen
|
||||
final List<String> scopes = getScopes(operation);
|
||||
if (scopes != null) {
|
||||
for (final String scope : scopes) {
|
||||
if (EntityMatcher.forRsql(scope).match(entity)) {
|
||||
if (QLSupport.getInstance().entityMatcher(scope, queryFieldType).match(entity)) {
|
||||
return; // at least one scope matches, operation is allowed
|
||||
}
|
||||
}
|
||||
@@ -110,7 +109,7 @@ public class DefaultAccessController<A extends Enum<A> & QueryField, T> implemen
|
||||
// * in case the entity permission(s) are implied - e.g. there is READ_REPOSITORY which implies READ_DISTRIBUTION_SET
|
||||
log.debug(
|
||||
"[{}] No matching authority found for operation {} (expects {}), they shall have already been checked with @PreAuthorize)",
|
||||
rsqlQueryFieldType, operation, operationPermissions);
|
||||
queryFieldType, operation, operationPermissions);
|
||||
return null;
|
||||
} else if (scopes.contains(null)) {
|
||||
return null; // not scoped at all
|
||||
|
||||
@@ -55,6 +55,7 @@ import org.eclipse.hawkbit.repository.RepositoryConstants;
|
||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||
import org.eclipse.hawkbit.repository.SecurityTokenGeneratorHolder;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.UpdateMode;
|
||||
import org.eclipse.hawkbit.repository.event.EventPublisherHolder;
|
||||
@@ -78,7 +79,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction_;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
|
||||
@@ -391,7 +392,7 @@ public class JpaControllerManagement extends JpaActionManagement implements Cont
|
||||
if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) {
|
||||
for (final PollingTime.Override override : pollingTime.getOverrides()) {
|
||||
try {
|
||||
if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) {
|
||||
if (QLSupport.getInstance().entityMatcher(override.qlStr(), TargetFields.class).match(jpaTarget)) {
|
||||
return override.pollingInterval().getFormattedIntervalWithDeviation(minPollingTime, maxPollingTime);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
|
||||
@@ -117,7 +117,7 @@ class JpaTargetFilterQueryManagement
|
||||
@Override
|
||||
public void verifyTargetFilterQuerySyntax(final String query) {
|
||||
try {
|
||||
QLSupport.getInstance().validateQuery(query, TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validate(query, TargetFields.class, JpaTarget.class);
|
||||
} catch (final RSQLParserException | RSQLParameterUnsupportedFieldException e) {
|
||||
log.debug("The RSQL query '{}}' is invalid.", query, e);
|
||||
throw new RSQLParameterSyntaxException("Cannot create a Rollout with an empty target query filter!");
|
||||
@@ -225,7 +225,7 @@ class JpaTargetFilterQueryManagement
|
||||
});
|
||||
Optional.ofNullable(create.getQuery()).ifPresent(query -> {
|
||||
// validate the RSQL query syntax
|
||||
QLSupport.getInstance().validateQuery(query, TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validate(query, TargetFields.class, JpaTarget.class);
|
||||
|
||||
// enforce the 'max targets per auto assign' quota right here even if the result of the filter query can vary over time
|
||||
Optional.ofNullable(create.getAutoAssignDistributionSet()).ifPresent(dsId -> {
|
||||
@@ -242,7 +242,7 @@ class JpaTargetFilterQueryManagement
|
||||
final JpaTargetFilterQuery targetFilterQuery = jpaRepository.getById(update.getId());
|
||||
Optional.ofNullable(update.getQuery()).ifPresent(query -> {
|
||||
// validate the RSQL query syntax
|
||||
QLSupport.getInstance().validateQuery(query, TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validate(query, TargetFields.class, JpaTarget.class);
|
||||
|
||||
Optional.ofNullable(targetFilterQuery.getAutoAssignDistributionSet()).ifPresent(autoAssignDs -> {
|
||||
// enforce the 'max targets per auto assignment'-quota only if the query is going to change
|
||||
|
||||
@@ -111,7 +111,7 @@ public class JpaTargetManagement
|
||||
@Override
|
||||
public boolean isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
|
||||
final String controllerId, final long distributionSetId, final String targetFilterQuery) {
|
||||
QLSupport.getInstance().validateQuery(targetFilterQuery, TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validate(targetFilterQuery, TargetFields.class, JpaTarget.class);
|
||||
final DistributionSet ds = distributionSetManagement.get(distributionSetId);
|
||||
final Long distSetTypeId = ds.getType().getId();
|
||||
final List<Specification<JpaTarget>> specList = List.of(
|
||||
|
||||
@@ -29,6 +29,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.im.authentication.SpPermission;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.event.remote.TenantConfigurationDeletedEvent;
|
||||
import org.eclipse.hawkbit.repository.event.remote.entity.RemoteEntityEvent;
|
||||
@@ -39,7 +40,7 @@ import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
|
||||
import org.eclipse.hawkbit.repository.jpa.executor.AfterTransactionCommitExecutor;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTenantConfiguration;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
|
||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||
import org.eclipse.hawkbit.repository.jpa.repository.TenantConfigurationRepository;
|
||||
import org.eclipse.hawkbit.repository.model.PollStatus;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
@@ -204,7 +205,7 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
|
||||
if (!ObjectUtils.isEmpty(pollingTime.getOverrides()) && target instanceof JpaTarget jpaTarget) {
|
||||
for (final PollingTime.Override override : pollingTime.getOverrides()) {
|
||||
try {
|
||||
if (EntityMatcher.forRsql(override.qlStr()).match(jpaTarget)) {
|
||||
if (QLSupport.getInstance().entityMatcher(override.qlStr(), TargetFields.class).match(jpaTarget)) {
|
||||
return pollStatus(lastTargetQuery, override.pollingInterval(), pollingOverdueTime);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
@@ -271,8 +272,8 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
|
||||
if (!ObjectUtils.isEmpty(pollingTime.getOverrides())) {
|
||||
// validate that the QL strings are valid RSQL queries,
|
||||
// nevertheless always when parse them we shall be prepared to catch exceptions if the parsers
|
||||
// has been changed in non backward compatible way
|
||||
pollingTime.getOverrides().forEach(override -> EntityMatcher.forRsql(override.qlStr()));
|
||||
// has been changed in not backward compatible way
|
||||
pollingTime.getOverrides().forEach(override -> QLSupport.getInstance().entityMatcher(override.qlStr(), TargetFields.class));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +292,8 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
|
||||
return jpaTenantConfigurations.stream().collect(Collectors.toMap(
|
||||
JpaTenantConfiguration::getKey,
|
||||
updatedTenantConfiguration -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
final Class<T> clazzT = (Class<T>) configurations.get(updatedTenantConfiguration.getKey()).getClass();
|
||||
@SuppressWarnings("unchecked") final Class<T> clazzT = (Class<T>) configurations.get(updatedTenantConfiguration.getKey())
|
||||
.getClass();
|
||||
return TenantConfigurationValue.<T> builder().global(false)
|
||||
.createdBy(updatedTenantConfiguration.getCreatedBy())
|
||||
.createdAt(updatedTenantConfiguration.getCreatedAt())
|
||||
@@ -351,7 +352,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
|
||||
if (MULTI_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) {
|
||||
JpaTenantConfiguration batchConfig = tenantConfigurationRepository.findByKey(BATCH_ASSIGNMENTS_ENABLED);
|
||||
if (batchConfig != null && Boolean.parseBoolean(batchConfig.getValue())) {
|
||||
log.debug("The Multi-Assignments '{}' feature cannot be enabled as it contradicts with the Batch-Assignments feature, which is already enabled .", key);
|
||||
log.debug(
|
||||
"The Multi-Assignments '{}' feature cannot be enabled as it contradicts with the Batch-Assignments feature, which is already enabled .",
|
||||
key);
|
||||
throw new TenantConfigurationValueChangeNotAllowedException();
|
||||
}
|
||||
}
|
||||
@@ -361,7 +364,9 @@ public class JpaTenantConfigurationManagement implements TenantConfigurationMana
|
||||
if (BATCH_ASSIGNMENTS_ENABLED.equals(key) && Boolean.parseBoolean(valueChange.getValue())) {
|
||||
JpaTenantConfiguration multiConfig = tenantConfigurationRepository.findByKey(MULTI_ASSIGNMENTS_ENABLED);
|
||||
if (multiConfig != null && Boolean.parseBoolean(multiConfig.getValue())) {
|
||||
log.debug("The Batch-Assignments '{}' feature cannot be enabled as it contradicts with the Multi-Assignments feature, which is already enabled .", key);
|
||||
log.debug(
|
||||
"The Batch-Assignments '{}' feature cannot be enabled as it contradicts with the Multi-Assignments feature, which is already enabled .",
|
||||
key);
|
||||
throw new TenantConfigurationValueChangeNotAllowedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,26 +375,26 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
*/
|
||||
@Test
|
||||
void rsqlValidTargetFields() {
|
||||
QLSupport.getInstance().validateQuery(
|
||||
QLSupport.getInstance().validate(
|
||||
"ID == '0123' and NAME == abcd and DESCRIPTION == absd and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" +
|
||||
" and CONTROLLERID == 0123 and UPDATESTATUS == PENDING and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" +
|
||||
" and tag == beta",
|
||||
TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validateQuery(
|
||||
QLSupport.getInstance().validate(
|
||||
"ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123 and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123",
|
||||
TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validateQuery(
|
||||
QLSupport.getInstance().validate(
|
||||
"ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg",
|
||||
TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validateQuery(
|
||||
QLSupport.getInstance().validate(
|
||||
"CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}",
|
||||
TargetFields.class, JpaTarget.class);
|
||||
QLSupport.getInstance().validateQuery(
|
||||
QLSupport.getInstance().validate(
|
||||
"ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg",
|
||||
TargetFields.class, JpaTarget.class);
|
||||
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> QLSupport.getInstance().validateQuery("wrongfield == abcd", TargetFields.class, JpaTarget.class));
|
||||
.isThrownBy(() -> QLSupport.getInstance().validate("wrongfield == abcd", TargetFields.class, JpaTarget.class));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,6 +449,6 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
private void assertRSQLQueryThrowsException(final String rsql) {
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> QLSupport.getInstance().validateQuery(rsql, TargetFields.class, JpaTarget.class));
|
||||
.isThrownBy(() -> QLSupport.getInstance().validate(rsql, TargetFields.class, JpaTarget.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user