implementation of record security locks, and permissions

This commit is contained in:
2023-01-11 10:24:31 -06:00
parent e4d37e3db9
commit 23e9abeb74
83 changed files with 6639 additions and 504 deletions

View File

@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate;
import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
@ -51,7 +52,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@ -299,11 +303,35 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
**
** method that sub-classes should call to make a full WHERE clause, including
** security clauses.
*******************************************************************************/
protected String makeWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException, QException
protected String makeWhereClause(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException, QException
{
String clause = makeSimpleWhereClause(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
String whereClauseWithoutSecurity = makeWhereClauseWithoutSecurity(instance, table, joinsContext, filter, params);
QQueryFilter securityFilter = getSecurityFilter(instance, session, table, joinsContext);
if(securityFilter == null || CollectionUtils.nullSafeIsEmpty(securityFilter.getCriteria()))
{
return (whereClauseWithoutSecurity);
}
String securityWhereClause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, securityFilter.getCriteria(), QQueryFilter.BooleanOperator.AND, params);
return ("(" + whereClauseWithoutSecurity + ") AND (" + securityWhereClause + ")");
}
/*******************************************************************************
** private method for making the part of a where clause that gets AND'ed to the
** security clause. Recursively handles sub-clauses.
*******************************************************************************/
private String makeWhereClauseWithoutSecurity(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List<Serializable> params) throws IllegalArgumentException, QException
{
if(filter == null || !filter.hasAnyCriteria())
{
return ("1 = 1");
}
String clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params);
if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters()))
{
///////////////////////////////////////////////////////////////
@ -322,7 +350,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
}
for(QQueryFilter subFilter : filter.getSubFilters())
{
String subClause = makeWhereClause(instance, table, joinsContext, subFilter, params);
String subClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, subFilter, params);
if(StringUtils.hasContent(subClause))
{
clauses.add("(" + subClause + ")");
@ -333,10 +361,129 @@ public abstract class AbstractRDBMSAction implements QActionInterface
/*******************************************************************************
** Build a QQueryFilter to apply record-level security to the query.
** Note, it may be empty, if there are no lock fields, or all are all-access.
*******************************************************************************/
private QQueryFilter getSecurityFilter(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext)
{
QQueryFilter newFilter = new QQueryFilter();
newFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
List<QFilterCriteria> securityCriteria = new ArrayList<>();
newFilter.setCriteria(securityCriteria);
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{
addCriteriaForRecordSecurityLock(instance, session, table, securityCriteria, recordSecurityLock, joinsContext, table.getName());
}
for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins()))
{
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
{
addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias());
}
}
return (newFilter);
}
/*******************************************************************************
**
*******************************************************************************/
private String makeSimpleWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List<QFilterCriteria> criteria, QQueryFilter.BooleanOperator booleanOperator, List<Serializable> params) throws IllegalArgumentException
private static void addCriteriaForRecordSecurityLock(QInstance instance, QSession session, QTableMetaData table, List<QFilterCriteria> securityCriteria, RecordSecurityLock recordSecurityLock, JoinsContext joinsContext, String tableNameOrAlias)
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check if the key type has an all-access key, and if so, if it's set to true for the current user/session //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN))
{
///////////////////////////////////////////////////////////////////////////////
// if we have all-access on this key, then we don't need a criterion for it. //
///////////////////////////////////////////////////////////////////////////////
return;
}
}
if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinChain()))
{
for(String joinName : recordSecurityLock.getJoinChain())
{
QJoinMetaData joinMetaData = instance.getJoin(joinName);
/*
for(QueryJoin queryJoin : joinsContext.getQueryJoins())
{
if(queryJoin.getJoinMetaData().getName().equals(joinName))
{
joinMetaData = queryJoin.getJoinMetaData();
break;
}
}
*/
if(joinMetaData == null)
{
throw (new RuntimeException("Could not find joinMetaData for recordSecurityLock with joinChain member [" + joinName + "]"));
}
table = instance.getTable(joinMetaData.getRightTable());
tableNameOrAlias = table.getName();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// else - get the key values from the session and decide what kind of criterion to build //
///////////////////////////////////////////////////////////////////////////////////////////
List<Serializable> securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), table.getField(recordSecurityLock.getFieldName()).getType());
String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName();
if(CollectionUtils.nullSafeIsEmpty(securityKeyValues))
{
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
{
securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
else
{
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. //
// todo - make some explicit contradiction here - maybe even avoid running the whole query - as you're not allowed ANY records //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, Collections.emptyList()));
}
}
else
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// else, if user/session has some values, build an IN rule - //
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
{
securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}
else
{
securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues));
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private String getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List<QFilterCriteria> criteria, QQueryFilter.BooleanOperator booleanOperator, List<Serializable> params) throws IllegalArgumentException
{
List<String> clauses = new ArrayList<>();
for(QFilterCriteria criterion : criteria)
@ -366,10 +513,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface
{
if(values.isEmpty())
{
//////////////////////////////////////////////////////////////////////////////////
// if there are no values, then we want a false here - so say column != column. //
//////////////////////////////////////////////////////////////////////////////////
clause += " != " + column;
///////////////////////////////////////////////////////
// if there are no values, then we want a false here //
///////////////////////////////////////////////////////
clause = " 0 = 1 ";
}
else
{
@ -377,14 +524,24 @@ public abstract class AbstractRDBMSAction implements QActionInterface
}
break;
}
case IS_NULL_OR_IN:
{
clause += " IS NULL ";
if(!values.isEmpty())
{
clause += " OR " + column + " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")";
}
break;
}
case NOT_IN:
{
if(values.isEmpty())
{
/////////////////////////////////////////////////////////////////////////////////
// if there are no values, then we want a true here - so say column == column. //
/////////////////////////////////////////////////////////////////////////////////
clause += " = " + column;
//////////////////////////////////////////////////////
// if there are no values, then we want a true here //
//////////////////////////////////////////////////////
clause = " 1 = 1 ";
}
else
{

View File

@ -74,10 +74,7 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega
QQueryFilter filter = aggregateInput.getFilter();
List<Serializable> params = new ArrayList<>();
if(filter != null && filter.hasAnyCriteria())
{
sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), table, joinsContext, filter, params);
}
sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), aggregateInput.getSession(), table, joinsContext, filter, params);
if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupBys()))
{

View File

@ -64,10 +64,7 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf
QQueryFilter filter = countInput.getFilter();
List<Serializable> params = new ArrayList<>();
if(filter != null && filter.hasAnyCriteria())
{
sql += " WHERE " + makeWhereClause(countInput.getInstance(), table, joinsContext, filter, params);
}
sql += " WHERE " + makeWhereClause(countInput.getInstance(), countInput.getSession(), table, joinsContext, filter, params);
// todo sql customization - can edit sql and/or param list

View File

@ -262,7 +262,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte
String tableName = getTableName(table);
JoinsContext joinsContext = new JoinsContext(deleteInput.getInstance(), table.getName(), Collections.emptyList());
String whereClause = makeWhereClause(deleteInput.getInstance(), table, joinsContext, filter, params);
String whereClause = makeWhereClause(deleteInput.getInstance(), deleteInput.getSession(), table, joinsContext, filter, params);
// todo sql customization - can edit sql and/or param list?
String sql = "DELETE FROM "

View File

@ -77,10 +77,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf
QQueryFilter filter = queryInput.getFilter();
List<Serializable> params = new ArrayList<>();
if(filter != null && filter.hasAnyCriteria())
{
sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), table, joinsContext, filter, params));
}
sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), queryInput.getSession(), table, joinsContext, filter, params));
if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys()))
{

View File

@ -27,6 +27,7 @@ import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
@ -35,8 +36,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
@ -60,6 +62,8 @@ public class TestUtils
public static final String TABLE_NAME_ITEM = "item";
public static final String TABLE_NAME_ORDER_LINE = "orderLine";
public static final String SECURITY_KEY_STORE_ALL_ACCESS = "storeAllAccess";
/*******************************************************************************
@ -219,21 +223,25 @@ public class TestUtils
qInstance.addTable(defineBaseTable(TABLE_NAME_STORE, "store")
.withRecordLabelFormat("%s")
.withRecordLabelFields("name")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("id"))
.withField(new QFieldMetaData("name", QFieldType.STRING))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
.withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
.withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
);
qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line")
.withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId"))
.withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id"))
.withField(new QFieldMetaData("sku", QFieldType.STRING))
.withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE))
@ -295,6 +303,11 @@ public class TestUtils
.withTableName(TABLE_NAME_STORE)
.withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY)
);
qInstance.addSecurityKeyType(new QSecurityKeyType()
.withName(TABLE_NAME_STORE)
.withAllAccessKeyName(SECURITY_KEY_STORE_ALL_ACCESS)
.withPossibleValueSourceName(TABLE_NAME_STORE));
}

View File

@ -48,6 +48,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@ -318,7 +319,18 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest
AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
AggregateResult aggregateResult = aggregateOutput.getResults().get(0);
assertNull(aggregateResult.getAggregateValue(sumOfQuantity));
aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
aggregateResult = aggregateOutput.getResults().get(0);
Assertions.assertEquals(43, aggregateResult.getAggregateValue(sumOfQuantity));
aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
aggregateResult = aggregateOutput.getResults().get(0);
// note - this would be 33, except for that one order line that has a contradictory store id...
Assertions.assertEquals(32, aggregateResult.getAggregateValue(sumOfQuantity));
}
@ -340,7 +352,10 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest
aggregateInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE));
AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
assertEquals(6, aggregateOutput.getResults().size());
assertEquals(0, aggregateOutput.getResults().size());
aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput);
assertSkuQuantity("QM-1", 30, aggregateOutput.getResults(), groupBy);
assertSkuQuantity("QM-2", 1, aggregateOutput.getResults(), groupBy);
assertSkuQuantity("QM-3", 1, aggregateOutput.getResults(), groupBy);

View File

@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -179,4 +180,26 @@ public class RDBMSCountActionTest extends RDBMSActionTest
assertEquals(2, countOutput.getCount(), "Right Join count should find 2 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurity() throws QException
{
CountInput countInput = new CountInput();
countInput.setInstance(TestUtils.defineInstance());
countInput.setTableName(TestUtils.TABLE_NAME_ORDER);
countInput.setSession(new QSession());
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(0);
countInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(8);
countInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(2, 3)));
assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(5);
}
}

View File

@ -22,8 +22,11 @@
package com.kingsrook.qqq.backend.module.rdbms.actions;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
@ -38,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.module.rdbms.TestUtils;
@ -605,6 +609,38 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testNestedFilterAndTopLevelFilter() throws QException
{
QueryInput queryInput = initQueryRequest();
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 3))
.withBooleanOperator(QQueryFilter.BooleanOperator.AND)
.withSubFilters(List.of(
new QQueryFilter()
.withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James")))
.withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))),
new QQueryFilter()
.withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff")))
.withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Chamberlain")))
))
);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row");
assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain"));
queryInput.getFilter().setCriteria(List.of(new QFilterCriteria("id", QCriteriaOperator.NOT_EQUALS, 3)));
queryOutput = new QueryAction().execute(queryInput);
assertEquals(0, queryOutput.getRecords().size(), "Next complex query should find 0 rows");
}
/*******************************************************************************
**
*******************************************************************************/
@ -712,7 +748,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
@Test
void testFiveTableOmsJoinFindMismatchedStoreId() throws Exception
{
QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession());
QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_STORE).withAlias("orderStore").withSelect(true));
queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true));
@ -754,7 +790,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
orderLineCount.set(rs.getInt(1));
});
QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession());
QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE);
queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true));
@ -775,7 +811,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
void testOmsQueryByPersons() throws Exception
{
QInstance instance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(instance, new QSession());
QueryInput queryInput = new QueryInput(instance, new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
/////////////////////////////////////////////////////
@ -877,7 +913,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
void testOmsQueryByPersonsExtraKelkhoffOrder() throws Exception
{
QInstance instance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(instance, new QSession());
QueryInput queryInput = new QueryInput(instance, new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -953,4 +989,330 @@ public class RDBMSQueryActionTest extends RDBMSActionTest
.hasRootCauseMessage("Duplicate table name or alias: shipToPerson");
}
/*******************************************************************************
** queries on the store table, where the primary key (id) is the security field
*******************************************************************************/
@Test
void testRecordSecurityPrimaryKeyFieldNoFilters() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_STORE);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(3);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(1)
.anyMatch(r -> r.getValueInteger("id").equals(1));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(1)
.anyMatch(r -> r.getValueInteger("id").equals(2));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(2)
.anyMatch(r -> r.getValueInteger("id").equals(1))
.anyMatch(r -> r.getValueInteger("id").equals(3));
}
/*******************************************************************************
** not really expected to be any different from where we filter on the primary key,
** but just good to make sure
*******************************************************************************/
@Test
void testRecordSecurityForeignKeyFieldNoFilters() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(8);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(3)
.allMatch(r -> r.getValueInteger("storeId").equals(1));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(2)
.allMatch(r -> r.getValueInteger("storeId").equals(2));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList()));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(6)
.allMatch(r -> r.getValueInteger("storeId").equals(1) || r.getValueInteger("storeId").equals(3));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityWithFilters() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(2)
.allMatch(r -> r.getValueInteger("storeId").equals(1));
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(3)
.allMatch(r -> r.getValueInteger("storeId").equals(1));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityWithOrQueries() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter(
new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)),
new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5))
).withBooleanOperator(QQueryFilter.BooleanOperator.OR));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(5)
.allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5));
queryInput.setFilter(new QQueryFilter(
new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)),
new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5))
).withBooleanOperator(QQueryFilter.BooleanOperator.OR));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(1)
.allMatch(r -> r.getValueInteger("storeId").equals(2))
.allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityWithSubFilters() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter()
.withBooleanOperator(QQueryFilter.BooleanOperator.OR)
.withSubFilters(List.of(
new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, 2), new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, 1)),
new QQueryFilter(new QFilterCriteria("billToPersonId", QCriteriaOperator.IS_BLANK), new QFilterCriteria("shipToPersonId", QCriteriaOperator.IS_BLANK)).withBooleanOperator(QQueryFilter.BooleanOperator.OR)
)));
Predicate<QRecord> p = r -> r.getValueInteger("billToPersonId") == null || r.getValueInteger("shipToPersonId") == null || (r.getValueInteger("id") >= 2 && r.getValueInteger("billToPersonId") == 1);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(4)
.allMatch(p);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(1)
.allMatch(r -> r.getValueInteger("storeId").equals(1))
.allMatch(p);
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(3)
.allMatch(r -> r.getValueInteger("storeId").equals(3))
.allMatch(p);
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityNullValues() throws Exception
{
runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (9, NULL, 1, 6)", null);
runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (10, NULL, 6, 5)", null);
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
Predicate<QRecord> hasNullStoreId = r -> r.getValueInteger("storeId") == null;
////////////////////////////////////////////
// all-access user should get all 10 rows //
////////////////////////////////////////////
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(10)
.anyMatch(hasNullStoreId);
//////////////////////////////////////////////////////////////////////////////////////////////////
// no-values user should get 0 rows (given that default null-behavior on this key type is DENY) //
//////////////////////////////////////////////////////////////////////////////////////////////////
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// user with list of all ids shouldn't see the nulls (given that default null-behavior on this key type is DENY) //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(8)
.noneMatch(hasNullStoreId);
//////////////////////////////////////////////////////////////////////////
// specifically set the null behavior to deny - repeat the last 2 tests //
//////////////////////////////////////////////////////////////////////////
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY);
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(8)
.noneMatch(hasNullStoreId);
///////////////////////////////////
// change null behavior to ALLOW //
///////////////////////////////////
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW);
/////////////////////////////////////////////
// all-access user should still get all 10 //
/////////////////////////////////////////////
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(10)
.anyMatch(hasNullStoreId);
/////////////////////////////////////////////////////
// no-values user should only get the rows w/ null //
/////////////////////////////////////////////////////
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(2)
.allMatch(hasNullStoreId);
////////////////////////////////////////////////////
// user with list of all ids should see the nulls //
////////////////////////////////////////////////////
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(10)
.anyMatch(hasNullStoreId);
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testRecordSecurityWithLockFromJoinTable() throws QException
{
QInstance qInstance = TestUtils.defineInstance();
QueryInput queryInput = new QueryInput(qInstance);
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
/////////////////////////////////////////////////////////////////////////////////////////////////
// remove the normal lock on the order table - replace it with one from the joined store table //
/////////////////////////////////////////////////////////////////////////////////////////////////
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().clear();
qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withRecordSecurityLock(new RecordSecurityLock()
.withSecurityKeyType(TestUtils.TABLE_NAME_STORE)
.withJoinChain(List.of("orderJoinStore"))
.withFieldName("id"));
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6);
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(2)
.allMatch(r -> r.getValueInteger("storeId").equals(1));
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5));
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7))));
queryInput.setSession(new QSession());
assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty();
queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2))));
queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3)));
assertThat(new QueryAction().execute(queryInput).getRecords())
.hasSize(3)
.allMatch(r -> r.getValueInteger("storeId").equals(1));
}
}

View File

@ -196,7 +196,7 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest
private String runReport(QInstance qInstance) throws QException
{
ReportInput reportInput = new ReportInput(qInstance);
reportInput.setSession(new QSession());
reportInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true));
reportInput.setReportName(TEST_REPORT);
reportInput.setReportFormat(ReportFormat.CSV);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();