Skip to content

Commit

Permalink
finish TypeConditions intersection
Browse files Browse the repository at this point in the history
  • Loading branch information
JafarMirzaie committed Jul 22, 2022
1 parent 2293a2c commit 15024fa
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 21 deletions.
20 changes: 12 additions & 8 deletions Signum.Engine.Extensions/Authorization/TypeAuthCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,13 @@ public TypeAuthCache(SchemaBuilder sb, IMerger<Type, TypeAllowedAndConditions> m
{
TypeConditionSymbol condition = (TypeConditionSymbol)arg;

var command = Administrator.UnsafeDeletePreCommandMList((RuleTypeEntity rt)=>rt.Conditions, Database.MListQuery((RuleTypeEntity rt) => rt.Conditions).Where(mle => mle.Element.Conditions.Contains(condition)));
if (!Database.MListQuery((RuleTypeConditionEntity rt) => rt.Conditions).Any(mle => mle.Element.Is(condition)))
return null;

return command;
var mlist = Administrator.UnsafeDeletePreCommandMList((RuleTypeConditionEntity rt)=>rt.Conditions, Database.MListQuery((RuleTypeConditionEntity rt) => rt.Conditions).Where(mle => mle.Element.Is(condition)));
var emptyRules = Administrator.UnsafeDeletePreCommand(Database.Query<RuleTypeConditionEntity>().Where(rt => rt.Conditions.Count == 0), force: true, avoidMList: true);

return SqlPreCommand.Combine(Spacing.Simple, mlist, emptyRules);
}

TypeAllowedAndConditions IManualAuth<Type, TypeAllowedAndConditions>.GetAllowed(Lite<RoleEntity> role, Type key)
Expand Down Expand Up @@ -327,7 +331,7 @@ orderby resource
new XAttribute("Allowed", allowed.Fallback.ToString()!),
from c in allowed.ConditionRules
select new XElement("Condition",
new XAttribute("Name", c.ToString()),
new XAttribute("Name", c.TypeConditions.ToString(", ")),
new XAttribute("Allowed", c.Allowed.ToString()))
)
)
Expand All @@ -351,7 +355,7 @@ from c in allowed.ConditionRules
TypeLogic.NameToType.Where(a => !a.Value.IsEnumEntity()).Select(a => a.Key).ToHashSet(), typeReplacementKey);

replacements.AskForReplacements(
xRoles.SelectMany(x => x.Elements("Type")).SelectMany(t => t.Elements("Condition")).SelectMany(x => x.Attribute("Name")!.Value.Split(" & ").ToList()).ToHashSet(),
xRoles.SelectMany(x => x.Elements("Type")).SelectMany(t => t.Elements("Condition")).SelectMany(x => x.Attribute("Name")!.Value.SplitNoEmpty(",").Select(a=>a.Trim()).ToList()).ToHashSet(),
SymbolLogic<TypeConditionSymbol>.AllUniqueKeys(),
typeConditionReplacementKey);

Expand All @@ -366,7 +370,7 @@ from c in allowed.ConditionRules
};


return Synchronizer.SynchronizeScript(Spacing.Double, should, current,
return Synchronizer.SynchronizeScript(Spacing.Triple, should, current,
createNew: (role, x) =>
{
var dic = (from xr in x.Elements("Type")
Expand Down Expand Up @@ -397,7 +401,7 @@ from c in allowed.ConditionRules
select KeyValuePair.Create(t, xr)).ToDictionaryEx("Type rules for {0}".FormatWith(role));
SqlPreCommand? restSql = Synchronizer.SynchronizeScript(
Spacing.Simple,
Spacing.Triple,
dic,
list.Where(a => a.Resource != null).ToDictionary(a => a.Resource),
createNew: (r, xr) =>
Expand Down Expand Up @@ -432,8 +436,8 @@ private static MList<RuleTypeConditionEntity> Conditions(XElement xr, Replacemen
var conditions = (from xc in xr.Elements("Condition")
select new RuleTypeConditionEntity
{
Conditions = xc.Attribute("Name")!.Value.Split(" & ")
.Select(s => SymbolLogic<TypeConditionSymbol>.TryToSymbol(replacements.Apply(typeConditionReplacementKey, s))).NotNull().ToMList(),
Conditions = xc.Attribute("Name")!.Value.SplitNoEmpty(",")
.Select(s => SymbolLogic<TypeConditionSymbol>.TryToSymbol(replacements.Apply(typeConditionReplacementKey, s.Trim()))).NotNull().ToMList(),
Allowed = xc.Attribute("Allowed")!.Value.ToEnum<TypeAllowed>()
}).ToMList();
return conditions;
Expand Down
19 changes: 17 additions & 2 deletions Signum.Engine.Extensions/Authorization/TypeAuthLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,21 @@ public static TypeAllowedAndConditions MergeBase(IEnumerable<TypeAllowedAndCondi
return GetRules(maxMatrix, numCells, conditionDictionary);
}

static string Debug(int cell, Dictionary<TypeConditionSymbol, int> conditionDictionary)
{
return conditionDictionary.ToString(kvp => ((kvp.Value & cell) == kvp.Value ? " " : "!") + kvp.Key.ToString().After("."), " & ");
}

static string Debug(TypeAllowed[] matrix, Dictionary<TypeConditionSymbol, int> conditionDictionary)
{
return matrix.Select((ta, i) => Debug(i, conditionDictionary) + " => " + ta).ToString("\n");
}

static string Debug(TypeAllowed?[] matrix, Dictionary<TypeConditionSymbol, int> conditionDictionary)
{
return matrix.Select((ta, i) => Debug(i, conditionDictionary) + " => " + ta).ToString("\n");
}

static TypeAllowed[] GetMatrix(TypeAllowedAndConditions tac, int numCells, Dictionary<TypeConditionSymbol, int> conditionDictionary)
{
var matrix = 0.To(numCells).Select(a => tac.Fallback).ToArray();
Expand Down Expand Up @@ -335,15 +350,15 @@ static TypeAllowedAndConditions GetRules(TypeAllowed[] matrix, int numCells, Dic
}

//>= 2 Conditions
for (int numConditions = 2; numConditions < availableTypeConditions.Count; numConditions++)
for (int numConditions = 2; numConditions <= availableTypeConditions.Count; numConditions++)
{
foreach (var mask in GetMasksOf(numConditions, availableTypeConditions))
{
var ta = OnlyOneValue(mask);

if (ta.HasValue)
{
conditionRules.Add(new TypeConditionRuleModel(availableTypeConditions.Where(tc => (conditionDictionary[tc] & mask) == mask).ToArray(), ta.Value));
conditionRules.Add(new TypeConditionRuleModel(availableTypeConditions.Where(tc => (conditionDictionary[tc] & mask) == conditionDictionary[tc]).ToArray(), ta.Value));

ClearArray(mask);

Expand Down
33 changes: 26 additions & 7 deletions Signum.Engine.Extensions/Authorization/TypeConditionAlgebra.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,20 @@ public override bool IsMoreSimpleAndGeneralThan(TypeConditionNode og)
return false;
}

public override string ToString() => Nodes.Count == 0 ? "TRUE" : $"AND({Nodes.ToString(", ")})";
public override string ToString() => Nodes.Count == 0 ? "TRUE" :
Nodes.All(a => a is SymbolNode) ? $"AND({Nodes.ToString(", ")})" :
$"AND(\n{Nodes.ToString(",\n").Indent(3)}\n)";

public override int GetHashCode() => Nodes.Sum(a => a.GetHashCode()) + 5;
public override int GetHashCode() {

var hs = new HashCode();
hs.Add(100);
foreach (var item in Nodes)
{
hs.Add(item);
}
return hs.ToHashCode();
}

public override bool Equals(object? obj) => obj is AndNode other && other.Nodes.ToHashSet().SetEquals(other.Nodes);

Expand All @@ -63,10 +74,18 @@ public OrNode(IEnumerable<TypeConditionNode> nodes)

public override bool IsMoreSimpleAndGeneralThan(TypeConditionNode og) => false;

public override string ToString() => Nodes.Count == 0 ? "FALSE" : $"OR({Nodes.ToString(a => " " + a + " ", ", ")})";

public override int GetHashCode() => Nodes.Sum(a => a.GetHashCode());
public override string ToString() => Nodes.Count == 0 ? "FALSE" : $"OR(\n{Nodes.ToString(",\n").Indent(2)}\n)";

public override int GetHashCode()
{
var hs = new HashCode();
hs.Add(10);
foreach (var item in Nodes)
{
hs.Add(item);
}
return hs.ToHashCode();
}
public override bool Equals(object? obj) => obj is OrNode other && other.Nodes.ToHashSet().SetEquals(other.Nodes);
}

Expand Down Expand Up @@ -99,7 +118,7 @@ public SymbolNode(TypeConditionSymbol symbol)

public override bool? ConstantValue => null;

public override bool IsMoreSimpleAndGeneralThan(TypeConditionNode og) => false;
public override bool IsMoreSimpleAndGeneralThan(TypeConditionNode og) => og is AndNode an && an.Nodes.Contains(this);

public override string ToString() => Symbol.ToString();

Expand Down Expand Up @@ -187,7 +206,7 @@ public static Expression ToExpression(this TypeConditionNode node, Expression en
return Expression.Negate(nn.ToExpression(entity));

if (node is AndNode and)
return and.Nodes.Select(n => n.ToExpression(entity)).Aggregate(Expression.Add);
return and.Nodes.Select(n => n.ToExpression(entity)).Aggregate(Expression.And);

if (node is OrNode or)
return or.Nodes.Select(n => n.ToExpression(entity)).Aggregate(Expression.Or);
Expand Down
14 changes: 12 additions & 2 deletions Signum.Entities.Extensions/Authorization/ServicesData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ internal bool Exactly(TypeAllowed current)

public TypeAllowedAndConditions WithoutCondition(TypeConditionSymbol typeCondition)
{
return new TypeAllowedAndConditions(this.Fallback, this.ConditionRules.Where(a => !a.TypeConditions.Contains(typeCondition)));
return new TypeAllowedAndConditions(this.Fallback, this.ConditionRules.Select(a => a.WithoutCondition(typeCondition)).NotNull().ToMList());
}
}

Expand Down Expand Up @@ -293,9 +293,19 @@ public bool Equals(TypeConditionRuleModel? other)
Allowed.Equals(other.Allowed);
}


[AutoExpressionField]
public override string ToString() => As.Expression(() => TypeConditions.ToString(" & ") + " => " + Allowed);

internal TypeConditionRuleModel? WithoutCondition(TypeConditionSymbol typeCondition)
{
if (!TypeConditions.Contains(typeCondition))
return this;

if (TypeConditions.Count == 1)
return null;

return new TypeConditionRuleModel { TypeConditions = TypeConditions.Where(tc => !tc.Is(typeCondition)).ToMList() };
}
}

public enum AuthThumbnail
Expand Down
4 changes: 4 additions & 0 deletions Signum.React.Extensions/Authorization/Admin/AuthAdmin.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@
color:#bbb
}

.sf-auth-rules small.and {
color: #444
}

a.sf-auth-chooser
{
text-decoration: none !important;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ export default React.forwardRef(function TypesRulesPackControl({ ctx }: { ctx: T
<span className="sf-condition-icon" onClick={() => handleRemoveConditionClick(tctx.value.allowed, cr)}><FontAwesomeIcon icon="minus-circle" /></span>
&nbsp;
{cr.typeConditions.flatMap((tc, j) => [
<small key={j}> {getToString(tc.element)}</small>,
j < cr.typeConditions.length - 1 ? <span key={j + "$"}>&</span> : null])
<small className="mx-1" key={j}>{getToString(tc.element)}</small>,
j < cr.typeConditions.length - 1 ? <small className="and" key={j + "$"}>&</small> : null])
.notNull()}
</td>
<td style={{ textAlign: "center" }} className={masterClass}>
Expand Down

2 comments on commit 15024fa

@olmobrutall
Copy link
Collaborator

@olmobrutall olmobrutall commented on 15024fa Jul 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeCondition intersection

TypeConditions is the feature that allows Signum Framework to have row-level security.

A TypeConditon expresses a condition for a particular type that can be used to override the default access rule (Write / Read / None)

image

What is new with this change is that when adding a new rule for a type, you can select more than one type condition:

image

Producing the following result

image

Benefits

With this change you don't need to hard-code intersection type conditions anymore, since the infrastructure can do it.

In fact, some of the optimizations that before where made at the System.Linq.Expression level are now made at the TypeCondition level, so you need to remove the hard-coded type conditions!.

But more important, before this change merging two roles was... optimistic. If worked for the simple cases but in the general case it failed (Fallback=null) making the type completely disallowed for the role.

But thanks to TypeCondition intersections we allows to Merge any arbitrary set or roles.

Example

Imagine for some type we have four type conditions for: smart, ugly, german and spain

Image we have two roles:

  • Role A with the rules:
    image
  • Role B with the rules:
    image
  • What are the rules for a role Role A + B that is the union of Role A and Role B

The new merge algorithm explores all the combinations, takes the maximum (for Union) on each case and then infers the new rules.

image

Sometimes the result is anti-intuitive but hopefully correct.

How to Migrate

1. Export the AuthRules.xml before migration (or from LIVE environment)

2. SQL Migration / Synchronize

   'auth.RuleTypeConditions' has been renamed in Tables?
 0: auth.RuleTypeCondition (hit [Enter])
 1: auth.RuleTypeConditionConditions
 n: No rename, 'auth.RuleTypeConditions' was removed
0

   'ParentID' has been renamed in Columns:auth.RuleTypeCondition?
 0: RuleTypeID (hit [Enter])
 1: Ticks
 n: No rename, 'ParentID' was removed
0

   'ConditionID' has been renamed in Columns:auth.RuleTypeCondition?
 0: Ticks (hit [Enter])
 n: No rename, 'ConditionID' was removed
n

In the generated script replace this:

EXEC SP_RENAME 'auth.RuleTypeConditions' , 'RuleTypeCondition';
ALTER TABLE auth.RuleTypeCondition ADD Ticks BIGINT NOT NULL CONSTRAINT DF_TEMP_Ticks DEFAULT 0;
ALTER TABLE auth.RuleTypeCondition DROP CONSTRAINT [DF_TEMP_Ticks];
EXEC SP_RENAME 'auth.RuleTypeCondition.ParentID' , 'RuleTypeID', 'COLUMN';
ALTER TABLE auth.RuleTypeCondition DROP COLUMN ConditionID;

CREATE TABLE auth.RuleTypeConditionConditions(
ID INT IDENTITY NOT NULL,
ParentID INT NOT NULL,
[Order] INT NOT NULL,
TypeConditionID INT NOT NULL,
CONSTRAINT [PK_auth_RuleTypeConditionConditions] PRIMARY KEY CLUSTERED (ID ASC)
);

For this:

EXEC SP_RENAME 'auth.RuleTypeConditions' , 'RuleTypeCondition';
ALTER TABLE auth.RuleTypeCondition ADD Ticks BIGINT NOT NULL CONSTRAINT DF_TEMP_Ticks DEFAULT 0;
ALTER TABLE auth.RuleTypeCondition DROP CONSTRAINT [DF_TEMP_Ticks];
EXEC SP_RENAME 'auth.RuleTypeCondition.ParentID' , 'RuleTypeID', 'COLUMN';
-- Moved down
--  ALTER TABLE auth.RuleTypeCondition DROP COLUMN ConditionID;  
GO

CREATE TABLE auth.RuleTypeConditionConditions(
ID INT IDENTITY NOT NULL,
ParentID INT NOT NULL,
[Order] INT NOT NULL,
TypeConditionID INT NOT NULL,
CONSTRAINT [PK_auth_RuleTypeConditionConditions] PRIMARY KEY CLUSTERED (ID ASC)
);
GO

-- NEW INSERT / SELECT to fill the new MList table
INSERT INTO auth.RuleTypeConditionConditions(ParentID, [Order], TypeConditionID)
SELECT ID, 0,  ConditionID
FROM auth.RuleTypeCondition
GO


ALTER TABLE auth.RuleTypeCondition DROP COLUMN ConditionID; --Moved down

3. Export the AuthRules and check that is identical.

4. Remove hard-coded intersecton TypeConditions (if any)

Now if you have any TypeCondition that is an hard-coded intersection like:

 TypeConditionLogic.RegisterCompile(MyAppCondition.IsExpensive, (InvoiceEntity inv) => inv.TotalAmount > 5000);
 TypeConditionLogic.RegisterCompile(MyAppCondition.IsAuthorized, (InvoiceEntity inv) => inv.State = InvoiceState.Authorized);
 //TypeConditionLogic.RegisterCompile(MyAppCondition.IsAuthorized_IsExpensive, (InvoiceEntity inv) =>inv.InCondition(MyAppCondition.IsExpensive) && inv.InCondition(MyAppCondition.IsAuthorized));

You have to remove it and synchronize.

Then in the AuthRules replace:

<Condition Name="MyAppCondition.IsAuthorized_IsExpensive" Allowed="Read" />
<Condition Name="MyAppCondition.IsExpensive, MyAppCondition.IsAuthorized" Allowed="Read" />

5. Import Auth Rules

and check that the changes make sense

What's next?

Having a solid Role merging algorithm allows many possibilities in role management.

I think having users being restricted to one-role is an important limitation and in some applications could be useful to have multiple Use-case-roles that can be selected for each user, and virtual roles that are created under the covers for each combination of use cases.

Any idea or feedback?

Enjoy!

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on 15024fa Jul 31, 2022 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.