Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Optimize datastructure rule evaluation #1918

Merged
merged 1 commit into from Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
15 changes: 9 additions & 6 deletions backend/Origam.Rule/RowSecurityStateBuilder.cs
@@ -1,6 +1,6 @@
#region license
/*
Copyright 2005 - 2021 Advantage Solutions, s. r. o.
Copyright 2005 - 2023 Advantage Solutions, s. r. o.

This file is part of ORIGAM (http://www.origam.org).

Expand Down Expand Up @@ -41,6 +41,7 @@ public class RowSecurityStateBuilder
private XmlContainer originalData;
private XmlContainer actualData;
private bool isBuildable;
private RuleEvaluationCache ruleEvaluationCache;


public static RowSecurityState BuildFull(RuleEngine ruleEngine,
Expand Down Expand Up @@ -104,6 +105,7 @@ private RowSecurityStateBuilder(DataRow row, RuleEngine ruleEngine)
actualData = DatasetTools.GetRowXml(row,
row.HasVersion(DataRowVersion.Proposed)
? DataRowVersion.Proposed : DataRowVersion.Default);
ruleEvaluationCache = new RuleEvaluationCache();
isBuildable = true;
}

Expand All @@ -123,10 +125,10 @@ private RowSecurityStateBuilder AddMainEntityRowStateAndFormatting()
ForegroundColor = formatting.ForeColor.ToArgb(),
AllowDelete = ruleEngine.EvaluateRowLevelSecurityState(
originalData, actualData, null, CredentialType.Delete,
entityId, Guid.Empty, isNew),
entityId, Guid.Empty, isNew, ruleEvaluationCache),
AllowCreate = ruleEngine.EvaluateRowLevelSecurityState(
originalData, actualData, null, CredentialType.Create,
entityId, Guid.Empty, isNew)
entityId, Guid.Empty, isNew, ruleEvaluationCache)
};
return this;
}
Expand All @@ -147,12 +149,12 @@ private RowSecurityStateBuilder AddMainEntityFieldStates()
EvaluateRowLevelSecurityState(originalData,
actualData, col.ColumnName,
CredentialType.Update,
entityId, fieldId, isNew);
entityId, fieldId, isNew, ruleEvaluationCache);
bool allowRead = ruleEngine.
EvaluateRowLevelSecurityState(originalData,
actualData, col.ColumnName,
CredentialType.Read,
entityId, fieldId, isNew);
entityId, fieldId, isNew, ruleEvaluationCache);
EntityFormatting fieldFormatting = ruleEngine.
Formatting(actualData, entityId, fieldId, null);
string dynamicLabel = ruleEngine.DynamicLabel(
Expand Down Expand Up @@ -220,7 +222,8 @@ private RowSecurityStateBuilder AddRelations(object profileId)
CredentialType.Create,
childEntityId, Guid.Empty,
row.RowState == DataRowState.Added
|| row.RowState == DataRowState.Detached
|| row.RowState == DataRowState.Detached,
ruleEvaluationCache
);
Result.Relations.Add(new RelationSecurityState(
rel.ChildTable.TableName, allowRelationCreate));
Expand Down
90 changes: 73 additions & 17 deletions backend/Origam.Rule/RuleEngine.cs
Expand Up @@ -1759,23 +1759,43 @@ XmlContainer dataToUseForRule
dataToUseForRule, action.Rule, action.Roles, null);
}

public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContainer actualData, string field, CredentialType type, Guid entityId, Guid fieldId, bool isNewRow)
public bool EvaluateRowLevelSecurityState(XmlContainer originalData,
XmlContainer actualData, string field, CredentialType type,
Guid entityId, Guid fieldId, bool isNewRow,
RuleEvaluationCache ruleEvaluationCache = null)
{
ArrayList rules = new ArrayList();

IDataEntity entity = _persistence.SchemaProvider.RetrieveInstance(typeof(AbstractSchemaItem), new ModelElementKey(entityId)) as IDataEntity;

// field-level rules
if(field != null)
IDataEntityColumn column = null;
if (field != null)
{
// we retrieve the column from the child-items list
// this is very cost efficient, because when retrieving abstract columns (i.e. Id, RecordCreated, RecordUpdated), they are never cached
IDataEntityColumn column = entity.GetChildById(fieldId) as IDataEntityColumn;
// this is very cost efficient, because when retrieving
// abstract columns (i.e. Id, RecordCreated, RecordUpdated), they are never cached
column = entity.GetChildById(fieldId) as IDataEntityColumn;

// field not found, this would be e.g. a looked up column, which does not point to a real entity field id
// field not found, this would be e.g. a looked up column,
// which does not point to a real entity field id
if(column != null)
{
rules.AddRange(column.RowLevelSecurityRules);
if (column.RowLevelSecurityRules.Count == 0)
{
// shortcircuit processing of row level security rules
// for a column without it's own rules
Boolean? result = ruleEvaluationCache?.GetRulelessFieldResult(
entityId, type);
if (result != null)
{
return result.Value;
}
}
else
{
rules.AddRange(column.RowLevelSecurityRules);
}
}
}

Expand All @@ -1785,9 +1805,12 @@ public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContaine
{
rules.AddRange(entityRules);
}

// no rules - permit
if(rules.Count == 0) return true;
if (rules.Count == 0)
{
return true;
}

rules.Sort();

Expand All @@ -1800,7 +1823,8 @@ public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContaine
if(entityRule.DeleteCredential && type == CredentialType.Delete && isNewRow)
{
// always allow to delete new (not saved) records
return true;
return PutToRulelessCache(type, entityId,
ruleEvaluationCache, column, true);
}
else if(
(entityRule.UpdateCredential && type == CredentialType.Update)
Expand All @@ -1809,9 +1833,19 @@ public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContaine
|| (entityRule.DeleteCredential && type == CredentialType.Delete)
)
{
if(IsRowLevelSecurityRuleMatching(entityRule, entityRule.ValueType == CredentialValueType.ActualValue ? actualData : originalData))
Boolean? result = ruleEvaluationCache?.Get(entityRule, entityId);
if (result == null)
{
result = IsRowLevelSecurityRuleMatching(entityRule,
entityRule.ValueType == CredentialValueType.ActualValue ?
actualData : originalData);
ruleEvaluationCache?.Put(entityRule, entityId, result.Value);
}
if (result.Value)
{
return entityRule.Type == PermissionType.Permit;
return PutToRulelessCache(type, entityId,
ruleEvaluationCache, column,
entityRule.Type == PermissionType.Permit);
}
}
}
Expand All @@ -1825,9 +1859,19 @@ public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContaine
| (fieldRule.ReadCredential & type == CredentialType.Read)
)
{
if(IsRowLevelSecurityRuleMatching(fieldRule, fieldRule.ValueType == CredentialValueType.ActualValue ? actualData : originalData))
Boolean? result = ruleEvaluationCache?.Get(fieldRule, entityId);
if (result == null)
{
return fieldRule.Type == PermissionType.Permit;
result = IsRowLevelSecurityRuleMatching(fieldRule,
fieldRule.ValueType == CredentialValueType.ActualValue
? actualData : originalData);
ruleEvaluationCache?.Put(fieldRule, entityId, result.Value);
}
if (result.Value)
{
return PutToRulelessCache(type, entityId,
ruleEvaluationCache, column,
fieldRule.Type == PermissionType.Permit);
}
}
}
Expand All @@ -1836,14 +1880,26 @@ public bool EvaluateRowLevelSecurityState(XmlContainer originalData, XmlContaine
// no match
if(type == CredentialType.Read)
{
// permit for read
return true;
return PutToRulelessCache(type, entityId,
ruleEvaluationCache, column, true);
}
else
{
// deny for all the others
return false;
return PutToRulelessCache(type, entityId,
ruleEvaluationCache, column, false);
}
}

private static bool PutToRulelessCache(CredentialType type, Guid entityId,
RuleEvaluationCache ruleEvaluationCache, IDataEntityColumn column, bool value)
{
if (column?.RowLevelSecurityRules.Count == 0
&& ruleEvaluationCache != null)
{
ruleEvaluationCache.PutRulelessFieldResult(entityId, type,
value);
}
return value;
}

private bool IsRowLevelSecurityRuleMatching(AbstractEntitySecurityRule rule, XmlContainer data)
Expand Down
69 changes: 69 additions & 0 deletions backend/Origam.Rule/RuleEvaluationCache.cs
@@ -0,0 +1,69 @@
#region license
/*
Copyright 2005 - 2023 Advantage Solutions, s. r. o.

This file is part of ORIGAM (http://www.origam.org).

ORIGAM is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

ORIGAM 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with ORIGAM. If not, see <http://www.gnu.org/licenses/>.
*/
#endregion

using Origam.Schema.EntityModel;
using System;
using System.Collections.Generic;

namespace Origam.Rule
{
public class RuleEvaluationCache
{
private readonly Dictionary<Tuple<Guid, CredentialValueType, Guid>, bool>
rules = new();
private readonly Dictionary<Tuple<Guid, CredentialType>, bool>
rulelessFieldSecurityRuleResults = new();

public bool? Get(AbstractEntitySecurityRule rule, Guid entityId)
{
if (rules.TryGetValue(new Tuple<Guid, CredentialValueType,
Guid>(rule.Id, rule.ValueType, entityId), out var result))
{
return result;
}
return null;
}
public void Put(AbstractEntitySecurityRule rule, Guid entityId,
bool value)
{
rules.Add(new Tuple<Guid, CredentialValueType, Guid>
(rule.Id, rule.ValueType, entityId), value);
}

public bool? GetRulelessFieldResult(Guid entityId, CredentialType type)
{
if (rulelessFieldSecurityRuleResults.TryGetValue(
new Tuple<Guid, CredentialType>(entityId, type),
out var result))
{
return result;
}
return null;
}

public void PutRulelessFieldResult(Guid entityId, CredentialType type,
bool value)
{
rulelessFieldSecurityRuleResults.Add(
new Tuple<Guid, CredentialType>(entityId, type), value);
}
}
}
37 changes: 23 additions & 14 deletions backend/Origam.Server/Session Stores/SaveableSessionStore.cs
Expand Up @@ -233,9 +233,18 @@ internal virtual object Save()
}
}

public override IEnumerable<ChangeInfo> UpdateObject(string entity, object id, string property, object newValue)
public override IEnumerable<ChangeInfo> UpdateObject(
string entity, object id, string property, object newValue)
{
return UpdateObjectWithDependenies(entity, id,
property, newValue, true);
}

public IEnumerable<ChangeInfo>
UpdateObjectWithDependenies(
string entity, object id, string property, object newValue,
bool isTopLevel)
{
var result = new HashSet<ChangeInfo>();
InitializeFieldDependencies();
DataTable table = GetTable(entity, this.Data);
Guid dsEntityId = (Guid)table.ExtendedProperties["Id"];
Expand All @@ -248,33 +257,33 @@ public override IEnumerable<ChangeInfo> UpdateObject(string entity, object id, s
foreach(Guid dependentColumnId in _entityFieldDependencies[dsEntityId][fieldId])
{
string dependentColumnName = ColumnNameById(table, dependentColumnId);
IEnumerable<ChangeInfo> changes;
try
{
changes = this.UpdateObject(entity, id, dependentColumnName, null);
this.UpdateObjectWithDependenies(
entity, id, dependentColumnName, null, false);
}
catch (NullReferenceException e)
{
throw new NullReferenceException(
String.Format(Resources.ErrorDependentColumnNotFound,
dependentColumnName, property, entity, e.Message));
}

foreach (var o in changes)
{
result.Add(o);
}
}
}
}

IEnumerable<ChangeInfo> baseChanges = base.UpdateObject(entity, id, property, newValue);
foreach (var o in baseChanges)
// call actual UpdateObject, do rowstates only for toplevel
// (last) update
if (isTopLevel)
{
result.Add(o);
return base.UpdateObject(entity, id,
property, newValue);
}
else
{
base.UpdateObjectsWithoutGetChanges(entity, id, property, newValue);
return new List<ChangeInfo>();
}

return result;
}

private static string ColumnNameById(DataTable table, Guid columnId)
Expand Down