CE-882 Add MultiRecordSecurityLocks

This commit is contained in:
2024-04-25 12:02:51 -05:00
parent b1ba910ac7
commit fec96c39cb
2 changed files with 310 additions and 81 deletions

View File

@ -86,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView;
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData; import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; 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.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
@ -711,7 +712,6 @@ public class QInstanceValidator
{ {
String prefix = "Table " + table.getName() + " "; String prefix = "Table " + table.getName() + " ";
RECORD_SECURITY_LOCKS_LOOP:
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
{ {
if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)")) if(!assertCondition(recordSecurityLock != null, prefix + "has a null recordSecurityLock (did you mean to give it a null list of locks?)"))
@ -719,95 +719,126 @@ public class QInstanceValidator
continue; continue;
} }
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{ {
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName); validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix);
} }
else
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{ {
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName)) validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix);
{
hasAnyBadJoins = true;
}
} }
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// don't bother trying to validate field names if we know we have a bad join. //
////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
if(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
continue RECORD_SECURITY_LOCKS_LOOP;
}
}
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
else
{
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
}
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
} }
} }
/*******************************************************************************
**
*******************************************************************************/
private void validateMultiRecordSecurityLock(QInstance qInstance, QTableMetaData table, MultiRecordSecurityLock multiRecordSecurityLock, String prefix)
{
assertCondition(multiRecordSecurityLock.getOperator() != null, prefix + "has a MultiRecordSecurityLock that is missing an operator");
for(RecordSecurityLock lock : multiRecordSecurityLock.getLocks())
{
validateRecordSecurityLock(qInstance, table, lock, prefix);
}
}
/*******************************************************************************
**
*******************************************************************************/
private void validateRecordSecurityLock(QInstance qInstance, QTableMetaData table, RecordSecurityLock recordSecurityLock, String prefix)
{
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType"))
{
assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName);
}
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{
if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName))
{
hasAnyBadJoins = true;
}
}
String fieldName = recordSecurityLock.getFieldName();
////////////////////////////////////////////////////////////////////////////////
// don't bother trying to validate field names if we know we have a bad join. //
////////////////////////////////////////////////////////////////////////////////
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
{
if(fieldName.contains("."))
{
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
{
List<QueryJoin> joins = new ArrayList<>();
///////////////////////////////////////////////////////////////////////////////////////////////////
// ok - so - the join name chain is going to be like this: //
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
// - securityFieldName = order.clientId //
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
// and step (via tmpTable variable) back to the securityField //
///////////////////////////////////////////////////////////////////////////////////////////////////
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
Collections.reverse(joinNameChain);
QTableMetaData tmpTable = table;
for(String joinName : joinNameChain)
{
QJoinMetaData join = qInstance.getJoin(joinName);
if(join == null)
{
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
return;
}
if(join.getLeftTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join));
tmpTable = qInstance.getTable(join.getRightTable());
}
else if(join.getRightTable().equals(tmpTable.getName()))
{
joins.add(new QueryJoin(join.flip()));
tmpTable = qInstance.getTable(join.getLeftTable());
}
else
{
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
return;
}
}
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
else
{
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
{
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
}
}
}
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,198 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Subclass of RecordSecurityLock, for combining multiple locks using a boolean
** (AND/OR) condition. Note that the combined locks can themselves also be
** Multi-locks, thus creating a tree of locks.
*******************************************************************************/
public class MultiRecordSecurityLock extends RecordSecurityLock implements Cloneable
{
private List<RecordSecurityLock> locks = new ArrayList<>();
private BooleanOperator operator;
/*******************************************************************************
**
*******************************************************************************/
@Override
protected MultiRecordSecurityLock clone() throws CloneNotSupportedException
{
MultiRecordSecurityLock clone = (MultiRecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(locks != null)
{
clone.locks = new ArrayList<>();
for(RecordSecurityLock lock : locks)
{
clone.locks.add(lock.clone());
}
}
return (clone);
}
/*******************************************************************************
**
*******************************************************************************/
public enum BooleanOperator
{
AND,
OR;
/*******************************************************************************
**
*******************************************************************************/
public QQueryFilter.BooleanOperator toFilterOperator()
{
return switch(this)
{
case AND -> QQueryFilter.BooleanOperator.AND;
case OR -> QQueryFilter.BooleanOperator.OR;
};
}
}
////////////////////////////////
// todo - remove, this is POC //
////////////////////////////////
static
{
new QTableMetaData()
.withName("savedReport")
.withRecordSecurityLock(new MultiRecordSecurityLock()
.withLocks(List.of(
new RecordSecurityLock()
.withFieldName("userId")
.withSecurityKeyType("user")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE),
new RecordSecurityLock()
.withFieldName("sharedReport.userId")
.withJoinNameChain(List.of("reportJoinSharedReport"))
.withSecurityKeyType("user")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE), // dynamic, from a value...
new RecordSecurityLock()
.withFieldName("sharedReport.groupId")
.withJoinNameChain(List.of("reportJoinSharedReport"))
.withSecurityKeyType("group")
.withNullValueBehavior(NullValueBehavior.DENY)
.withLockScope(LockScope.READ_AND_WRITE) // dynamic, from a value...
)));
}
/*******************************************************************************
** Getter for locks
*******************************************************************************/
public List<RecordSecurityLock> getLocks()
{
return (this.locks);
}
/*******************************************************************************
** Setter for locks
*******************************************************************************/
public void setLocks(List<RecordSecurityLock> locks)
{
this.locks = locks;
}
/*******************************************************************************
** Fluent setter for locks
*******************************************************************************/
public MultiRecordSecurityLock withLocks(List<RecordSecurityLock> locks)
{
this.locks = locks;
return (this);
}
/*******************************************************************************
** Fluently add one lock
*******************************************************************************/
public MultiRecordSecurityLock withLock(RecordSecurityLock lock)
{
if(this.locks == null)
{
this.locks = new ArrayList<>();
}
this.locks.add(lock);
return (this);
}
/*******************************************************************************
** Getter for operator
*******************************************************************************/
public BooleanOperator getOperator()
{
return (this.operator);
}
/*******************************************************************************
** Setter for operator
*******************************************************************************/
public void setOperator(BooleanOperator operator)
{
this.operator = operator;
}
/*******************************************************************************
** Fluent setter for operator
*******************************************************************************/
public MultiRecordSecurityLock withOperator(BooleanOperator operator)
{
this.operator = operator;
return (this);
}
}