diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 85c7e307..96ae3df3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -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.scheduleing.QScheduleMetaData; 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.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; @@ -711,7 +712,6 @@ public class QInstanceValidator { String prefix = "Table " + table.getName() + " "; - RECORD_SECURITY_LOCKS_LOOP: 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?)")) @@ -719,95 +719,126 @@ public class QInstanceValidator continue; } - String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); - if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType")) + if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock) { - assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName); + validateMultiRecordSecurityLock(qInstance, table, multiRecordSecurityLock, prefix); } - - 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())) + else { - if(!assertCondition(qInstance.getJoin(joinName) != null, prefix + "has an unrecognized joinName: " + joinName)) - { - hasAnyBadJoins = true; - } + validateRecordSecurityLock(qInstance, table, recordSecurityLock, prefix); } - - 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 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 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 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 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"); + } + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java new file mode 100644 index 00000000..04cb945b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/MultiRecordSecurityLock.java @@ -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 . + */ + +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 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 getLocks() + { + return (this.locks); + } + + + + /******************************************************************************* + ** Setter for locks + *******************************************************************************/ + public void setLocks(List locks) + { + this.locks = locks; + } + + + + /******************************************************************************* + ** Fluent setter for locks + *******************************************************************************/ + public MultiRecordSecurityLock withLocks(List 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); + } + +}