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

Roles Updating and History #13040

Merged
merged 18 commits into from
Jan 10, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/OrchardCore.Modules/OrchardCore.Recipes/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@

[assembly: Feature(
Id = "OrchardCore.Recipes.Core",
Name = "Recipes",
Description = "Provides recipe services.",
Name = "Recipes Core Services",
Description = "Provides recipe core services.",
Category = "Infrastructure",
EnabledByDependencyOnly = true
)]
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
using Microsoft.Extensions.Localization;
using OrchardCore.Data.Documents;
using OrchardCore.DisplayManagement.Notify;
using OrchardCore.Documents;
using OrchardCore.Environment.Extensions;
using OrchardCore.Environment.Extensions.Features;
using OrchardCore.Roles.Models;
using OrchardCore.Roles.ViewModels;
using OrchardCore.Security;
using OrchardCore.Security.Permissions;
Expand All @@ -24,39 +22,35 @@ namespace OrchardCore.Roles.Controllers
public class AdminController : Controller
{
private readonly IDocumentStore _documentStore;
private readonly IAuthorizationService _authorizationService;
private readonly IStringLocalizer S;
private readonly RoleManager<IRole> _roleManager;
private readonly IDocumentManager<RolesDocument> _rolesDocumentManager;
private readonly IAuthorizationService _authorizationService;
private readonly IEnumerable<IPermissionProvider> _permissionProviders;
private readonly ITypeFeatureProvider _typeFeatureProvider;
private readonly IRoleService _roleService;
private readonly INotifier _notifier;
private readonly IStringLocalizer S;
private readonly IHtmlLocalizer H;

public AdminController(
IAuthorizationService authorizationService,
ITypeFeatureProvider typeFeatureProvider,
IDocumentStore documentStore,
IStringLocalizer<AdminController> stringLocalizer,
IHtmlLocalizer<AdminController> htmlLocalizer,
RoleManager<IRole> roleManager,
IDocumentManager<RolesDocument> rolesDocumentManager,
IAuthorizationService authorizationService,
IEnumerable<IPermissionProvider> permissionProviders,
ITypeFeatureProvider typeFeatureProvider,
IRoleService roleService,
INotifier notifier,
IEnumerable<IPermissionProvider> permissionProviders
)
IStringLocalizer<AdminController> stringLocalizer,
IHtmlLocalizer<AdminController> htmlLocalizer)
{
H = htmlLocalizer;
_notifier = notifier;
_roleService = roleService;
_typeFeatureProvider = typeFeatureProvider;
_permissionProviders = permissionProviders;
_documentStore = documentStore;
_roleManager = roleManager;
_rolesDocumentManager = rolesDocumentManager;
S = stringLocalizer;
_authorizationService = authorizationService;
_documentStore = documentStore;
_permissionProviders = permissionProviders;
_typeFeatureProvider = typeFeatureProvider;
_roleService = roleService;
_notifier = notifier;
S = stringLocalizer;
H = htmlLocalizer;
}

public async Task<ActionResult> Index()
Expand Down Expand Up @@ -211,42 +205,13 @@ public async Task<IActionResult> EditPost(string id, string roleDescription)

role.RoleDescription = roleDescription;

var rolesDocument = await _rolesDocumentManager.GetOrCreateMutableAsync();
var updateRolesDocument = false;

rolesDocument.PermissionGroups.TryAdd(role.RoleName, new List<string>());

var permissionNames = _permissionProviders.SelectMany(x => x.GetDefaultStereotypes())
.SelectMany(y => y.Permissions ?? Enumerable.Empty<Permission>())
.Select(x => x.Name)
.ToList();

// Save
// Save.
var rolePermissions = new List<RoleClaim>();
foreach (var key in Request.Form.Keys)
{
var permissionName = key;

if (key.StartsWith("Checkbox.", StringComparison.Ordinal))
{
permissionName = key.Substring("Checkbox.".Length);
}

if (!permissionNames.Contains(permissionName, StringComparer.OrdinalIgnoreCase))
{
// The request contains an invalid permission, let's ignore it
continue;
}

if (!rolesDocument.PermissionGroups[role.RoleName].Contains(permissionName, StringComparer.OrdinalIgnoreCase))
{
// Save the permission in the permissions history so it never gets auto assigned to this role.
rolesDocument.PermissionGroups[role.RoleName].Add(permissionName);
updateRolesDocument = true;
}

if (String.Equals(Request.Form[key], "true", StringComparison.OrdinalIgnoreCase))
if (key.StartsWith("Checkbox.", StringComparison.Ordinal) && Request.Form[key] == "true")
{
var permissionName = key["Checkbox.".Length..];
rolePermissions.Add(new RoleClaim { ClaimType = Permission.ClaimType, ClaimValue = permissionName });
}
}
Expand All @@ -256,11 +221,6 @@ public async Task<IActionResult> EditPost(string id, string roleDescription)

await _roleManager.UpdateAsync(role);

if (updateRolesDocument)
{
await _rolesDocumentManager.UpdateAsync(rolesDocument);
}

await _notifier.SuccessAsync(H["Role updated successfully."]);

return RedirectToAction(nameof(Index));
Expand Down
17 changes: 16 additions & 1 deletion src/OrchardCore.Modules/OrchardCore.Roles/Manifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
Author = ManifestConstants.OrchardCoreTeam,
Website = ManifestConstants.OrchardCoreWebsite,
Version = ManifestConstants.OrchardCoreVersion,
Description = "The roles module adds the permissions to assign roles to users. It's also provides a set of default roles for which other modules can define default permissions.",
Category = "Security"
)]

[assembly: Feature(
Id = "OrchardCore.Roles",
Name = "Roles",
Description = "Provides permissions to assign roles to users. Additionally, it updates default roles with default permissions provided by features.",
Dependencies = new[] { "OrchardCore.Roles.Core" },
Category = "Security"
)]

[assembly: Feature(
Id = "OrchardCore.Roles.Core",
Name = "Roles Core Services",
Description = "Provides role core services.",
EnabledByDependencyOnly = true,
Category = "Security"
)]
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
using System;
using System.Collections.Generic;
using OrchardCore.Data.Documents;
using OrchardCore.Security;
using OrchardCore.Security.Permissions;

namespace OrchardCore.Roles.Models
{
public class RolesDocument : Document
{
public List<Role> Roles { get; set; } = new List<Role>();


/// <summary>
/// Keeps track of all permission that were automaticly assigned to a role using <see cref="IPermissionProvider"></see>/>
/// </summary>
public Dictionary<string, List<string>> PermissionGroups { get; set; } = new(StringComparer.OrdinalIgnoreCase);
public List<Role> Roles { get; set; } = new();
public Dictionary<string, List<string>> MissingFeaturesByRole { get; set; } = new();
Copy link
Member

Choose a reason for hiding this comment

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

It seems weird to track missing items instead of what we know.

Copy link
Member Author

Choose a reason for hiding this comment

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

See my last comment for the logic where we only use feature / role events, when creating a role we enlist the missing features (installed but not enabled) and only those defining providers, so that when re-enabling such a feature we can lazily update roles that missed it on creation.

Not the only reason but for example doing the opposite would be harder to manage existing tenants, would need a "migration" path to init the history for all existing roles assuming that all installed features was already used, so only using the events would not be sufficient.

Also doing this way, on startup nothing is done on existing tenants until a new role is created, also on a fresh setup this history is just empty, only populated when we disable a feature that provide perms for a given default role that doesn't exist yet, and then create this default role.

In summary when only using events, we need these missing features while creating a role, before it was not needed as default roles was auto created by features.

That's why I first kept the feature Installed event as before, and then because we now can create a default role later on, we need these missing features (installed but no more enabled) to lazily "install" them for this role when they are re-activated.

Copy link
Member Author

Choose a reason for hiding this comment

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

That's said I may have done something wrong as there are so many scenarios I tried to keep while still fixing the 2 current issues. YOLO ;)

}
}
15 changes: 13 additions & 2 deletions src/OrchardCore.Modules/OrchardCore.Roles/Services/RoleStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,17 @@ public async Task<IdentityResult> CreateAsync(IRole role, CancellationToken canc
throw new ArgumentNullException(nameof(role));
}

var roleToCreate = (Role)role;
Copy link
Member

Choose a reason for hiding this comment

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

Maybe here we should use is directive

Copy link
Member Author

Choose a reason for hiding this comment

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

Didn't want to change the behavior but will check

Copy link
Member Author

Choose a reason for hiding this comment

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

Just used the same pattern as used below for the existing IRoleRemovedEventHandler.

        var roleToRemove = (Role)role;


var roles = await LoadRolesAsync();
roles.Roles.Add((Role)role);
roles.Roles.Add(roleToCreate);
await UpdateRolesAsync(roles);

var roleCreatedEventHandlers = _serviceProvider.GetRequiredService<IEnumerable<IRoleCreatedEventHandler>>();

await roleCreatedEventHandlers.InvokeAsync((handler, roleToCreate) =>
handler.RoleCreatedAsync(roleToCreate.RoleName), roleToCreate, _logger);

return IdentityResult.Success;
}

Expand All @@ -90,7 +97,9 @@ public async Task<IdentityResult> DeleteAsync(IRole role, CancellationToken canc
}

var roleRemovedEventHandlers = _serviceProvider.GetRequiredService<IEnumerable<IRoleRemovedEventHandler>>();
await roleRemovedEventHandlers.InvokeAsync((handler, roleToRemove) => handler.RoleRemovedAsync(roleToRemove.RoleName), roleToRemove, _logger);

await roleRemovedEventHandlers.InvokeAsync((handler, roleToRemove) =>
handler.RoleRemovedAsync(roleToRemove.RoleName), roleToRemove, _logger);

var roles = await LoadRolesAsync();
roleToRemove = roles.Roles.FirstOrDefault(r => r.RoleName == roleToRemove.RoleName);
Expand Down Expand Up @@ -252,8 +261,10 @@ public async Task<IdentityResult> UpdateAsync(IRole role, CancellationToken canc

#endregion IRoleClaimStore<IRole>

#pragma warning disable CA1816
public void Dispose()
{
}
#pragma warning restore CA1816
}
}
Loading