CE-882 Add filterForReadLockTree (required cloning in RecordSecurityLock)

This commit is contained in:
2024-04-25 12:03:34 -05:00
parent 1e1b660979
commit 5fe95ab0be
3 changed files with 235 additions and 1 deletions

View File

@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.security; package com.kingsrook.qqq.backend.core.model.metadata.security;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -41,7 +42,7 @@ import java.util.Map;
** - READ_AND_WRITE means that users cannot read or write records without a valid key. ** - READ_AND_WRITE means that users cannot read or write records without a valid key.
** - WRITE means that users cannot write records without a valid key (but they can read them). ** - WRITE means that users cannot write records without a valid key (but they can read them).
*******************************************************************************/ *******************************************************************************/
public class RecordSecurityLock public class RecordSecurityLock implements Cloneable
{ {
private String securityKeyType; private String securityKeyType;
private String fieldName; private String fieldName;
@ -50,6 +51,26 @@ public class RecordSecurityLock
private LockScope lockScope = LockScope.READ_AND_WRITE; private LockScope lockScope = LockScope.READ_AND_WRITE;
/*******************************************************************************
**
*******************************************************************************/
@Override
protected RecordSecurityLock clone() throws CloneNotSupportedException
{
RecordSecurityLock clone = (RecordSecurityLock) super.clone();
/////////////////////////
// deep-clone the list //
/////////////////////////
if(joinNameChain != null)
{
clone.joinNameChain = new ArrayList<>();
clone.joinNameChain.addAll(joinNameChain);
}
return (clone);
}
/******************************************************************************* /*******************************************************************************
@ -265,4 +286,22 @@ public class RecordSecurityLock
return (this); return (this);
} }
/*******************************************************************************
**
*******************************************************************************/
@Override
public String toString()
{
return "RecordSecurityLock{" +
"securityKeyType='" + securityKeyType + '\'' +
", fieldName='" + fieldName + '\'' +
", joinNameChain=" + joinNameChain +
", nullValueBehavior=" + nullValueBehavior +
", lockScope=" + lockScope +
'}';
}
} }

View File

@ -46,6 +46,41 @@ public class RecordSecurityLockFilters
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to reads.
*******************************************************************************/
public static MultiRecordSecurityLock filterForReadLockTree(List<RecordSecurityLock> recordSecurityLocks)
{
if(recordSecurityLocks == null)
{
return (null);
}
MultiRecordSecurityLock result = new MultiRecordSecurityLock();
result.setOperator(MultiRecordSecurityLock.BooleanOperator.AND);
for(RecordSecurityLock recordSecurityLock : recordSecurityLocks)
{
if(recordSecurityLock instanceof MultiRecordSecurityLock multiRecordSecurityLock)
{
MultiRecordSecurityLock filteredSubLock = filterForReadLockTree(multiRecordSecurityLock.getLocks());
filteredSubLock.setOperator(multiRecordSecurityLock.getOperator());
result.withLock(filteredSubLock);
}
else
{
if(RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
result.withLock(recordSecurityLock);
}
}
}
return (result);
}
/******************************************************************************* /*******************************************************************************
** filter a list of locks so that we only see the ones that apply to writes. ** filter a list of locks so that we only see the ones that apply to writes.
*******************************************************************************/ *******************************************************************************/

View File

@ -0,0 +1,160 @@
/*
* 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.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.AND;
import static com.kingsrook.qqq.backend.core.model.metadata.security.MultiRecordSecurityLock.BooleanOperator.OR;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/*******************************************************************************
** Unit test for RecordSecurityLockFilters
*******************************************************************************/
class RecordSecurityLockFiltersTest extends BaseTest
{
/*******************************************************************************
**
*******************************************************************************/
@Test
void test()
{
MultiRecordSecurityLock nullBecauseNull = RecordSecurityLockFilters.filterForReadLockTree(null);
assertNull(nullBecauseNull);
MultiRecordSecurityLock emptyBecauseEmptyList = RecordSecurityLockFilters.filterForReadLockTree(List.of());
assertEquals(0, emptyBecauseEmptyList.getLocks().size());
MultiRecordSecurityLock emptyBecauseAllWrite = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertEquals(0, emptyBecauseAllWrite.getLocks().size());
MultiRecordSecurityLock onlyA = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertMultiRecordSecurityLock(onlyA, AND, "A");
MultiRecordSecurityLock twoOutOfThreeTopLevel = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
));
assertMultiRecordSecurityLock(twoOutOfThreeTopLevel, AND, "A", "C");
MultiRecordSecurityLock treeOfAllReads = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
))
));
assertEquals(2, treeOfAllReads.getLocks().size());
assertEquals(AND, treeOfAllReads.getOperator());
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(0), OR, "A", "B");
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeOfAllReads.getLocks().get(1), OR, "C", "D");
MultiRecordSecurityLock treeWithOneBranchReadsOneBranchWrites = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
))
));
assertEquals(2, treeWithOneBranchReadsOneBranchWrites.getLocks().size());
assertEquals(AND, treeWithOneBranchReadsOneBranchWrites.getOperator());
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(0), OR, "A", "B");
assertMultiRecordSecurityLock((MultiRecordSecurityLock) treeWithOneBranchReadsOneBranchWrites.getLocks().get(1), OR);
MultiRecordSecurityLock deepSparseTree = RecordSecurityLockFilters.filterForReadLockTree(List.of(
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("A").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("B").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new RecordSecurityLock().withFieldName("C").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("D").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new MultiRecordSecurityLock().withOperator(OR).withLocks(List.of(
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("E").withLockScope(RecordSecurityLock.LockScope.WRITE),
new RecordSecurityLock().withFieldName("F").withLockScope(RecordSecurityLock.LockScope.WRITE)
)),
new MultiRecordSecurityLock().withOperator(AND).withLocks(List.of(
new RecordSecurityLock().withFieldName("G").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("H").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE)
))
)),
new RecordSecurityLock().withFieldName("I").withLockScope(RecordSecurityLock.LockScope.READ_AND_WRITE),
new RecordSecurityLock().withFieldName("J").withLockScope(RecordSecurityLock.LockScope.WRITE)
));
assertEquals(3, deepSparseTree.getLocks().size());
assertEquals(AND, deepSparseTree.getOperator());
MultiRecordSecurityLock deepChild0 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(0);
assertEquals(2, deepChild0.getLocks().size());
assertEquals(OR, deepChild0.getOperator());
MultiRecordSecurityLock deepGrandChild0 = (MultiRecordSecurityLock) deepChild0.getLocks().get(0);
assertMultiRecordSecurityLock(deepGrandChild0, AND, "A");
assertEquals("C", deepChild0.getLocks().get(1).getFieldName());
MultiRecordSecurityLock deepChild1 = (MultiRecordSecurityLock) deepSparseTree.getLocks().get(1);
assertEquals(2, deepChild1.getLocks().size());
assertEquals(OR, deepChild1.getOperator());
MultiRecordSecurityLock deepGrandChild1 = (MultiRecordSecurityLock) deepChild1.getLocks().get(0);
assertMultiRecordSecurityLock(deepGrandChild1, AND);
MultiRecordSecurityLock deepGrandChild2 = (MultiRecordSecurityLock) deepChild1.getLocks().get(1);
assertMultiRecordSecurityLock(deepGrandChild2, AND, "G", "H");
assertEquals("I", deepSparseTree.getLocks().get(2).getFieldName());
}
/*******************************************************************************
**
*******************************************************************************/
private void assertMultiRecordSecurityLock(MultiRecordSecurityLock lock, MultiRecordSecurityLock.BooleanOperator operator, String... lockFieldNames)
{
assertEquals(lockFieldNames.length, lock.getLocks().size());
assertEquals(operator, lock.getOperator());
for(int i = 0; i < lockFieldNames.length; i++)
{
assertEquals(lockFieldNames[i], lock.getLocks().get(i).getFieldName());
}
}
}