Skip to content

Commit

Permalink
add IsTrivialMerge to RoleEntity
Browse files Browse the repository at this point in the history
  • Loading branch information
olmobrutall committed Jul 27, 2022
1 parent d30864e commit c468115
Show file tree
Hide file tree
Showing 20 changed files with 285 additions and 81 deletions.
5 changes: 3 additions & 2 deletions Signum.Engine.Extensions/Authorization/AuthCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ public A GetAllowedBase(Lite<RoleEntity> role)

Dictionary<Lite<RoleEntity>, RoleAllowedCache> NewCache()
{
List<Lite<RoleEntity>> roles = AuthLogic.RolesInOrder().ToList();
List<Lite<RoleEntity>> roles = AuthLogic.RolesInOrder(includeTrivialMerge: true).ToList();

Dictionary<Lite<RoleEntity>, Dictionary<K, A>> realRules =
Database.Query<RT>()
Expand Down Expand Up @@ -282,8 +282,9 @@ internal XElement ExportXml(XName rootName, XName elementName, Func<K, string> r
var rules = runtimeRules.Value;

return new XElement(rootName,
(from r in AuthLogic.RolesInOrder()
(from r in AuthLogic.RolesInOrder(includeTrivialMerge: false)
let rac = rules.GetOrThrow(r)

select new XElement("Role",
new XAttribute("Name", r.ToString()!),
from k in allKeys ?? (rac.DefaultDictionary().OverrideDictionary?.Keys).EmptyIfNull()
Expand Down
177 changes: 131 additions & 46 deletions Signum.Engine.Extensions/Authorization/AuthLogic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ public static UserEntity? AnonymousUser
public static IQueryable<UserEntity> Users(this RoleEntity r) =>
As.Expression(() => Database.Query<UserEntity>().Where(u => u.Role.Is(r)));

static ResetLazy<DirectedGraph<Lite<RoleEntity>>> roles = null!;
static ResetLazy<DirectedGraph<Lite<RoleEntity>>> rolesGraph = null!;
static ResetLazy<DirectedGraph<Lite<RoleEntity>>> rolesInverse = null!;
static ResetLazy<Dictionary<string, Lite<RoleEntity>>> rolesByName = null!;
static ResetLazy<Dictionary<Lite<RoleEntity>, RoleEntity>> rolesByLite = null!;

class RoleData
{
Expand All @@ -68,6 +69,8 @@ public static void Start(SchemaBuilder sb, string? systemUserName, string? anony
SystemUserName = systemUserName;
AnonymousUserName = anonymousUserName;

RoleEntity.RetrieveFromCache = r => rolesByLite.Value.GetOrThrow(r);

UserWithClaims.FillClaims += (userWithClaims, user)=>
{
userWithClaims.Claims["Role"] = ((UserEntity)user).Role;
Expand Down Expand Up @@ -110,14 +113,15 @@ public static void Start(SchemaBuilder sb, string? systemUserName, string? anony
r.Description,
});

roles = sb.GlobalLazy(CacheRoles, new InvalidateWith(typeof(RoleEntity)), AuthLogic.NotifyRulesChanged);
rolesInverse = sb.GlobalLazy(() => roles.Value.Inverse(), new InvalidateWith(typeof(RoleEntity)));
rolesByName = sb.GlobalLazy(() => roles.Value.ToDictionaryEx(a => a.ToString()!), new InvalidateWith(typeof(RoleEntity)));
rolesByLite = sb.GlobalLazy(() => Database.Query<RoleEntity>().ToDictionaryEx(a => a.ToLite()), new InvalidateWith(typeof(RoleEntity)), AuthLogic.NotifyRulesChanged);
rolesByName = sb.GlobalLazy(() => rolesByLite.Value.Keys.ToDictionaryEx(a => a.ToString()!), new InvalidateWith(typeof(RoleEntity)));
rolesGraph = sb.GlobalLazy(()=> CacheRoles(rolesByLite.Value), new InvalidateWith(typeof(RoleEntity)));
rolesInverse = sb.GlobalLazy(() => rolesGraph.Value.Inverse(), new InvalidateWith(typeof(RoleEntity)));
mergeStrategies = sb.GlobalLazy(() =>
{
var strategies = Database.Query<RoleEntity>().Select(r => KeyValuePair.Create(r.ToLite(), r.MergeStrategy)).ToDictionary();
var graph = roles.Value;
var graph = rolesGraph.Value;
Dictionary<Lite<RoleEntity>, RoleData> result = new Dictionary<Lite<RoleEntity>, RoleData>();
foreach (var r in graph.CompilationOrder())
Expand All @@ -134,7 +138,7 @@ public static void Start(SchemaBuilder sb, string? systemUserName, string? anony
}
return result;
}, new InvalidateWith(typeof(RoleEntity)), AuthLogic.NotifyRulesChanged);
}, new InvalidateWith(typeof(RoleEntity)));

sb.Schema.EntityEvents<RoleEntity>().Saving += Schema_Saving;

Expand All @@ -154,48 +158,126 @@ public static void Start(SchemaBuilder sb, string? systemUserName, string? anony
}
}

public static Lite<RoleEntity> GetOrCreateTrivialMergeRole(List<Lite<RoleEntity>> roles)
{
if (roles.Count == 1)
return roles.SingleEx();

var flatRoles = roles
.Select(a => rolesByLite.Value.GetOrThrow(a))
.ToList()
.SelectMany(a => a.IsTrivialMerge ? a.InheritsFrom.ToArray() : new[] { a.ToLite() })
.Distinct()
.ToList();

if (flatRoles.Count == 1)
return flatRoles.SingleEx();

var newName = RoleEntity.CalculateTrivialMergeName(flatRoles);

var db = rolesByName.Value.TryGetC(newName);

if (db != null)
return db;

using (AuthLogic.Disable())
{
var result = new RoleEntity
{
Name = newName,
MergeStrategy = MergeStrategy.Union,
Description = null,
IsTrivialMerge = true,
InheritsFrom = flatRoles.ToMList()
}.Execute(RoleOperation.Save);

return result.ToLite();
}
}

static void Schema_Saving(RoleEntity role)
{
if (!role.IsNew && role.InheritsFrom.IsGraphModified)
if (!role.IsNew)
{
using (new EntityCache(EntityCacheType.ForceNew))
{
EntityCache.AddFullGraph(role);
var allRoles = Database.RetrieveAll<RoleEntity>();

var roleGraph = DirectedGraph<RoleEntity>.Generate(allRoles, r => r.InheritsFrom.Select(sr => sr.RetrieveAndRemember()));
if (role.InheritsFrom.IsGraphModified)
{
var roleGraph = DirectedGraph<RoleEntity>.Generate(allRoles, r => r.InheritsFrom.Select(sr => sr.RetrieveAndRemember()));

var problems = roleGraph.FeedbackEdgeSet().Edges.ToList();

if (problems.Count > 0)
throw new ApplicationException(
AuthAdminMessage._0CyclesHaveBeenFoundInTheGraphOfRolesDueToTheRelationships.NiceToString(problems.Count) +
problems.ToString("\r\n"));
}

var problems = roleGraph.FeedbackEdgeSet().Edges.ToList();
var dic = allRoles.ToDictionary(a => a.ToLite());

if (problems.Count > 0)
var problems2 = allRoles.SelectMany(r => r.InheritsFrom.Where(inh => rolesByLite.Value.GetOrThrow(inh).IsTrivialMerge).Select(inh => new { r, inh })).ToList();
if (problems2.Any())
throw new ApplicationException(
AuthAdminMessage._0CyclesHaveBeenFoundInTheGraphOfRolesDueToTheRelationships.NiceToString().FormatWith(problems.Count) +
problems.ToString("\r\n"));
problems2.GroupBy(a => a.r, a => a.inh)
.Select(gr => AuthAdminMessage.Role0InheritsFromTrivialMergeRole1.NiceToString(gr.Key, gr.CommaAnd()))
.ToString("\r\n"));
}

if (!role.IsTrivialMerge)
{
var trivialDependant = rolesInverse.Value.IndirectlyRelatedTo(role.ToLite())
.Select(r => rolesByLite.Value.GetOrThrow(r))
.Where(a => a.IsTrivialMerge);

if (trivialDependant.Any())
{
if (role.Name != role.InDB(a => a.Name))
{
foreach (var item in trivialDependant)
{
var replaced = item.InheritsFrom.Select(r => r.Is(role) ? role.ToLite() : r);

var newName = RoleEntity.CalculateTrivialMergeName(replaced);

item.InDB().UnsafeUpdate(a => a.Name, a => newName);
}
}
}
}
}
else
{
if (role.InheritsFrom.Any())
{
using (new EntityCache(EntityCacheType.ForceNew))
{
var problems = role.InheritsFrom.Where(a => a.EntityOrNull?.IsTrivialMerge ?? a.InDB(a => a.IsTrivialMerge)).ToList();

if (problems.Any())
{
throw new ApplicationException(AuthAdminMessage.Role0InheritsFromTrivialMergeRole1.NiceToString(role, problems.CommaAnd()));
}
}
}
}
}

static DirectedGraph<Lite<RoleEntity>> CacheRoles()
static DirectedGraph<Lite<RoleEntity>> CacheRoles(Dictionary<Lite<RoleEntity>, RoleEntity> rolesLite)
{
using (AuthLogic.Disable())
{
DirectedGraph<Lite<RoleEntity>> newRoles = new DirectedGraph<Lite<RoleEntity>>();
var graph = DirectedGraph<Lite<RoleEntity>>.Generate(rolesLite.Keys, r => rolesLite.GetOrThrow(r).InheritsFrom);

using (new EntityCache(EntityCacheType.ForceNewSealed))
foreach (var role in Database.RetrieveAll<RoleEntity>())
{
newRoles.Expand(role.ToLite(), r => r.RetrieveAndRemember().InheritsFrom);
}
var problems = graph.FeedbackEdgeSet().Edges.ToList();

var problems = newRoles.FeedbackEdgeSet().Edges.ToList();
if (problems.Count > 0)
throw new ApplicationException(
AuthAdminMessage._0CyclesHaveBeenFoundInTheGraphOfRolesDueToTheRelationships.NiceToString().FormatWith(problems.Count) +
problems.ToString("\r\n"));

if (problems.Count > 0)
throw new ApplicationException(
AuthAdminMessage._0CyclesHaveBeenFoundInTheGraphOfRolesDueToTheRelationships.NiceToString().FormatWith(problems.Count) +
problems.ToString("\r\n"));
return graph;

return newRoles;
}
}

public static IDisposable UnsafeUserSession(string username)
Expand Down Expand Up @@ -223,14 +305,15 @@ public static IDisposable UnsafeUserSession(string username)
return result;
}

public static IEnumerable<Lite<RoleEntity>> RolesInOrder()
public static IEnumerable<Lite<RoleEntity>> RolesInOrder(bool includeTrivialMerge)
{
return roles.Value.CompilationOrderGroups().SelectMany(gr => gr.OrderBy(a => a.ToString()));
return rolesGraph.Value.CompilationOrderGroups().SelectMany(gr => gr.OrderBy(a => a.ToString()))
.Where(r => includeTrivialMerge || !rolesByLite.Value.GetOrCreate(r).IsTrivialMerge);
}

internal static DirectedGraph<Lite<RoleEntity>> RolesGraph()
{
return roles.Value;
return rolesGraph.Value;
}

public static Lite<RoleEntity> GetRole(string roleName)
Expand All @@ -240,7 +323,7 @@ public static Lite<RoleEntity> GetRole(string roleName)

public static IEnumerable<Lite<RoleEntity>> RelatedTo(Lite<RoleEntity> role)
{
return roles.Value.RelatedTo(role);
return rolesGraph.Value.RelatedTo(role);
}

public static MergeStrategy GetMergeStrategy(Lite<RoleEntity> role)
Expand Down Expand Up @@ -389,12 +472,12 @@ public static void StartAllModules(SchemaBuilder sb, bool activeDirectoryIntegra

public static HashSet<Lite<RoleEntity>> CurrentRoles()
{
return roles.Value.IndirectlyRelatedTo(RoleEntity.Current, true);
return rolesGraph.Value.IndirectlyRelatedTo(RoleEntity.Current, true);
}

public static HashSet<Lite<RoleEntity>> IndirectlyRelated(Lite<RoleEntity> role)
{
return roles.Value.IndirectlyRelatedTo(role, true);
return rolesGraph.Value.IndirectlyRelatedTo(role, true);
}

public static HashSet<Lite<RoleEntity>> InverseIndirectlyRelated(Lite<RoleEntity> role)
Expand All @@ -404,7 +487,7 @@ public static HashSet<Lite<RoleEntity>> InverseIndirectlyRelated(Lite<RoleEntity

internal static int Rank(Lite<RoleEntity> role)
{
return roles.Value.IndirectlyRelatedTo(role).Count;
return rolesGraph.Value.IndirectlyRelatedTo(role).Count;
}

public static event Func<bool, XElement>? ExportToXml;
Expand All @@ -420,10 +503,11 @@ public static XDocument ExportRules(bool exportAll = false)
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("Auth",
new XElement("Roles",
RolesInOrder().Select(r => new XElement("Role",
RolesInOrder(includeTrivialMerge: true).Select(r => new XElement("Role",
new XAttribute("Name", r.ToString()!),
GetMergeStrategy(r) == MergeStrategy.Intersection ? new XAttribute("MergeStrategy", MergeStrategy.Intersection) : null!,
new XAttribute("Contains", roles.Value.RelatedTo(r).ToString(",")),
rolesByLite.Value.GetOrCreate(r).IsTrivialMerge ? new XAttribute("IsTrivialMerge", true) : null!,
new XAttribute("Contains", rolesGraph.Value.RelatedTo(r).ToString(",")),
rolesDic.TryGetC(r)?.Description?.Let(d => new XAttribute("Description", d))
))),
ExportToXml?.GetInvocationListTyped().Select(a => a(exportAll)).NotNull().OrderBy(a => a.Name.ToString())!));
Expand All @@ -433,7 +517,7 @@ public static XDocument ExportRules(bool exportAll = false)
{
Replacements replacements = new Replacements { Interactive = interactive };

Dictionary<string, Lite<RoleEntity>> rolesDic = roles.Value.ToDictionary(a => a.ToString()!);
Dictionary<string, Lite<RoleEntity>> rolesDic = rolesGraph.Value.ToDictionary(a => a.ToString()!);
Dictionary<string, XElement> rolesXml = doc.Root!.Element("Roles")!.Elements("Role").ToDictionary(x => x.Attribute("Name")!.Value);

replacements.AskForReplacements(rolesXml.Keys.ToHashSet(), rolesDic.Keys.ToHashSet(), "Roles");
Expand All @@ -450,14 +534,14 @@ public static XDocument ExportRules(bool exportAll = false)
{
var r = rolesDic.GetOrThrow(kvp.Key);

var current = GetMergeStrategy(r);
var should = kvp.Value.Attribute("MergeStrategy")?.Let(t => t.Value.ToEnum<MergeStrategy>()) ?? MergeStrategy.Union;

if (current != should)
throw new InvalidOperationException("Merge strategy of {0} is {1} in the database but is {2} in the file".FormatWith(r, current, should));
var currentMergeStrategy = GetMergeStrategy(r);
var shouldMergeStrategy = kvp.Value.Attribute("MergeStrategy")?.Let(t => t.Value.ToEnum<MergeStrategy>()) ?? MergeStrategy.Union;
if (currentMergeStrategy != shouldMergeStrategy)
throw new InvalidOperationException("Merge strategy of {0} is {1} in the database but is {2} in the file".FormatWith(r, currentMergeStrategy, shouldMergeStrategy));

EnumerableExtensions.JoinStrict(
roles.Value.RelatedTo(r),
rolesGraph.Value.RelatedTo(r),
kvp.Value.Attribute("Contains")!.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
sr => sr.ToString()!,
s => rolesDic.GetOrThrow(s).ToString()!,
Expand Down Expand Up @@ -500,6 +584,7 @@ public static void LoadRoles(XDocument doc)
{
Name = x.Attribute("Name")!.Value,
MergeStrategy = x.Attribute("MergeStrategy")?.Let(ms => ms.Value.ToEnum<MergeStrategy>()) ?? MergeStrategy.Union,
IsTrivialMerge = x.Attribute("IsTrivialMerge")?.Let(t => t.Value.ToBool()) ?? false,
SubRoles = x.Attribute("Contains")!.Value.SplitNoEmpty(','),
Description = x.Attribute("Description")?.Value,
}).ToList();
Expand Down Expand Up @@ -713,10 +798,10 @@ public static bool IsLogged()

public static int Compare(Lite<RoleEntity> role1, Lite<RoleEntity> role2)
{
if (roles.Value.IndirectlyRelatedTo(role1).Contains(role2))
if (rolesGraph.Value.IndirectlyRelatedTo(role1).Contains(role2))
return 1;

if (roles.Value.IndirectlyRelatedTo(role2).Contains(role1))
if (rolesGraph.Value.IndirectlyRelatedTo(role2).Contains(role1))
return -1;

return 0;
Expand Down
4 changes: 2 additions & 2 deletions Signum.Engine.Extensions/Authorization/TypeAuthCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Dictionary<Lite<RoleEntity>, RoleAllowedCache> NewCache()
using (AuthLogic.Disable())
using (new EntityCache(EntityCacheType.ForceNewSealed))
{
List<Lite<RoleEntity>> roles = AuthLogic.RolesInOrder().ToList();
List<Lite<RoleEntity>> roles = AuthLogic.RolesInOrder(includeTrivialMerge: true).ToList();

var rules = Database.Query<RuleTypeEntity>().ToList();

Expand Down Expand Up @@ -316,7 +316,7 @@ internal XElement ExportXml(List<Type>? allTypes)
var rules = runtimeRules.Value;

return new XElement("Types",
(from r in AuthLogic.RolesInOrder()
(from r in AuthLogic.RolesInOrder(includeTrivialMerge: false)
let rac = rules.GetOrThrow(r)
select new XElement("Role",
new XAttribute("Name", r.ToString()!),
Expand Down
9 changes: 8 additions & 1 deletion Signum.Entities.Extensions/Authorization/AuthMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,13 @@ public enum AuthAdminMessage
SelectOneToOverrideTheAccessFor0ThatSatisfyThisCondition,

[Description("Select more than one to override access for {0} that satisfy all the conditions at the same time.")]
SelectMoreThanOneToOverrideAccessFor0ThatSatisfyAllTheConditionsAtTheSameTime
SelectMoreThanOneToOverrideAccessFor0ThatSatisfyAllTheConditionsAtTheSameTime,

[Description("Role {0} inherits from trivial merge role {1}")]
Role0InheritsFromTrivialMergeRole1,

IncludeTrivialMerges,

[Description("Role {0} is trivial merge")]
Role0IsTrivialMerge,
}
Loading

2 comments on commit c468115

@olmobrutall
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

RoleEntity IsTrivialMerge

Applications with many roles and modules have traditionally been constrained in Signum Framework due to the limitaiton of a user having only one role.

Until last week, this limitation was a hard one, since merging arbitrary roles with different type conditions for the same type was not guarantee to succeed. But this is fortunately solved.

This change doesn't go all the way and changes the UserEntity to have an MList<Lite<RoleEntity>> yet for technical (AuthCache) and UX reasons (simple lists), but tries to make this scenarios easier.

The idea is that, when clicking in the find icon in the User role,

image

Now multiple roles can be selected.

image

In this case, a controller in the server will find a role that inherits from this two roles or, if it doesn't exist, create it...

image

But this union role needs to have some restrictions:

  • It should not have any overriden Authorization Rules of it's own, otherwise will be surprising / unexpected for the end user.
  • It should be a Union merge of more two roles or more.
  • It should have a unique name (even when inheriting from many roles!), without description, and efectively read-only to keep the name in sync with the data.
  • No avoid normalization problems, this union roles should not be inherited by anybody.
  • By default, this union roles should be hidden from the UI when searching for roles.

All this behaviour and constrains are controlled by a new internal field IsTrivialMerge to RoleEntity.

image

Conclusion

This change efectively separates the world of roles in two.

  • On one side, are real roles and use-case roles. The line between them is blurry so there is no differentiation.
  • On the other side, are roels that exist only because a subset of users would like to effectively have more than one role.

Hopefully this small change could bring some order to applications with many roles, and make Signum more Enterprise-Frendly when using standards like Active Directory groups.

@MehdyKarimpour
Copy link
Contributor

@MehdyKarimpour MehdyKarimpour commented on c468115 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.