mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
CE-882 Add MultiRecordSecurityLocks
This commit is contained in:
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Reference in New Issue
Block a user