From 009daa30af536801583ec423a9f7d4c9ab7c2c70 Mon Sep 17 00:00:00 2001 From: Mike Alhayek Date: Wed, 18 Jan 2023 08:39:50 -0800 Subject: [PATCH] Enhance User management by introducing List, Edit, Assign, and Delete permissions (#12407) --- .../OrchardCore.Users/AdminMenu.cs | 10 +- .../Controllers/AdminController.cs | 195 ++++++++---------- .../Controllers/RegistrationController.cs | 2 +- .../ChangeEmailSettingsDisplayDriver.cs | 4 +- .../Drivers/LoginSettingsDisplayDriver.cs | 8 +- .../RegistrationSettingsDisplayDriver.cs | 4 +- .../ResetPasswordSettingsDisplayDriver.cs | 4 +- .../Drivers/UserDisplayDriver.cs | 58 +++--- .../Drivers/UserInformationDisplayDriver.cs | 109 ++++++---- .../Drivers/UserRoleDisplayDriver.cs | 115 ++++++----- .../OrchardCore.Users/Models/LoginSettings.cs | 4 + .../OrchardCore.Users/Permissions.cs | 29 ++- .../DefaultUsersAdminListFilterProvider.cs | 16 +- .../DefaultUsersAdminListQueryService.cs | 9 +- .../Services/RoleAuthorizationHandler.cs | 127 ++++++++++++ .../Services/RolesAdminListFilterProvider.cs | 40 ++++ .../Services/UserAuthorizationHandler.cs | 113 ---------- .../OrchardCore.Users/Startup.cs | 19 +- .../OrchardCore.Users/UserRolePermissions.cs | 49 +++-- .../ViewModels/EditUserEmailViewModel.cs | 15 ++ .../ViewModels/EditUserNameViewModel.cs | 13 ++ .../ViewModels/EditUserRoleViewModel.cs | 5 +- .../Views/Admin/Display.cshtml | 1 + .../Views/Admin/Index.cshtml | 5 +- .../Views/LoginSettings.Edit.cshtml | 52 +++-- ...tionItemText-UserProfileSettings.Id.cshtml | 1 + .../Views/User.DetailAdmin.cshtml | 23 +++ .../Views/User.SummaryAdmin.cshtml | 45 ++-- .../Views/UserButtons.cshtml | 37 ++-- .../Views/UserEmail.Edit.cshtml | 10 + .../OrchardCore.Users/Views/UserInfo.cshtml | 15 ++ .../OrchardCore.Users/Views/UserMenu.cshtml | 10 +- .../Views/UserName.Edit.cshtml | 10 + .../Views/UserRoleFields.Edit.cshtml | 4 +- .../OrchardCore.Users/Views/UserRoles.cshtml | 21 ++ .../Views/UserRolesMeta.cshtml | 20 ++ .../Views/UsersAdminList.cshtml | 10 +- .../Views/UsersAdminListCreate.cshtml | 7 +- .../Security/Services/RoleHelper.cs | 14 ++ .../Services/RoleServiceExtensions.cs | 33 +++ .../CommonPermissions.cs | 46 ++++- .../UsersServiceCollectionExtensions.cs | 3 +- .../IUsersAdminListFilter.cs | 12 ++ src/docs/releases/1.6.0.md | 24 +++ 44 files changed, 889 insertions(+), 462 deletions(-) create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/RoleAuthorizationHandler.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/RolesAdminListFilterProvider.cs delete mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Services/UserAuthorizationHandler.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserEmailViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserNameViewModel.cs create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Display.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/NavigationItemText-UserProfileSettings.Id.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/User.DetailAdmin.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/UserEmail.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/UserInfo.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/UserName.Edit.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/UserRoles.cshtml create mode 100644 src/OrchardCore.Modules/OrchardCore.Users/Views/UserRolesMeta.cshtml create mode 100644 src/OrchardCore/OrchardCore.Infrastructure.Abstractions/Security/Services/RoleHelper.cs create mode 100644 src/OrchardCore/OrchardCore.Users.Core/IUsersAdminListFilter.cs diff --git a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs index bc590dfebd7..3aa32a383af 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/AdminMenu.cs @@ -29,13 +29,13 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Users"], S["Users"].PrefixPosition(), users => users .AddClass("users").Id("users") .Action("Index", "Admin", "OrchardCore.Users") - .Permission(Permissions.ViewUsers) + .Permission(CommonPermissions.ListUsers) .Resource(new User()) .LocalNav() ) .Add(S["Settings"], settings => settings .Add(S["User Login"], S["User Login"].PrefixPosition(), login => login - .Permission(Permissions.ManageUsers) + .Permission(CommonPermissions.ManageUsers) .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = LoginSettingsDisplayDriver.GroupId }) .LocalNav() ) @@ -67,7 +67,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Security"], security => security .Add(S["Settings"], settings => settings .Add(S["User Change email"], S["User Change email"].PrefixPosition(), registration => registration - .Permission(Permissions.ManageUsers) + .Permission(CommonPermissions.ManageUsers) .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = ChangeEmailSettingsDisplayDriver.GroupId }) .LocalNav() ))); @@ -97,7 +97,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Security"], security => security .Add(S["Settings"], settings => settings .Add(S["User Registration"], S["User Registration"].PrefixPosition(), registration => registration - .Permission(Permissions.ManageUsers) + .Permission(CommonPermissions.ManageUsers) .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = RegistrationSettingsDisplayDriver.GroupId }) .LocalNav() ))); @@ -127,7 +127,7 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .Add(S["Security"], security => security .Add(S["Settings"], settings => settings .Add(S["User Reset password"], S["User Reset password"].PrefixPosition(), password => password - .Permission(Permissions.ManageUsers) + .Permission(CommonPermissions.ManageUsers) .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = ResetPasswordSettingsDisplayDriver.GroupId }) .LocalNav() ))); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs index 150588098e9..7495c0c26c4 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/AdminController.cs @@ -62,7 +62,7 @@ public AdminController( INotifier notifier, IOptions pagerOptions, IShapeFactory shapeFactory, - ILogger logger, + ILogger logger, IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer, IUpdateModelAccessor updateModelAccessor) @@ -92,16 +92,17 @@ public async Task Index([ModelBinder(BinderType = typeof(UserFilte // Check a dummy user account to see if the current user has permission to view users. var authUser = new User(); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ViewUsers, authUser)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ListUsers, authUser)) { return Forbid(); } - var options = new UserIndexOptions(); - - // Populate route values to maintain previous route data when generating page links - // await _userOptionsDisplayManager.UpdateEditorAsync(options, _updateModelAccessor.ModelUpdater, false); - options.FilterResult = queryFilterResult; + var options = new UserIndexOptions + { + // Populate route values to maintain previous route data when generating page links + // await _userOptionsDisplayManager.UpdateEditorAsync(options, _updateModelAccessor.ModelUpdater, false); + FilterResult = queryFilterResult + }; options.FilterResult.MapTo(options); // With the options populated we filter the query, allowing the filters to alter the options. @@ -135,8 +136,7 @@ public async Task Index([ModelBinder(BinderType = typeof(UserFilte { UserId = user.UserId, Shape = await _userDisplayManager.BuildDisplayAsync(user, updater: _updateModelAccessor.ModelUpdater, displayType: "SummaryAdmin") - } - ); + }); } options.UserFilters = new List() @@ -166,16 +166,16 @@ public async Task Index([ModelBinder(BinderType = typeof(UserFilte }; var allRoles = (await _roleService.GetRoleNamesAsync()) - .Except(new[] { "Anonymous", "Authenticated" }, StringComparer.OrdinalIgnoreCase); + .Except(RoleHelper.SystemRoleNames, StringComparer.OrdinalIgnoreCase); options.UserRoleFilters = new List() { new SelectListItem() { Text = S["All roles"], Value = String.Empty, Selected = (options.SelectedRole == String.Empty) }, - new SelectListItem() { Text = S["Authenticated (no roles)"], Value = "Authenticated", Selected = (String.Equals(options.SelectedRole, "Authenticated", StringComparison.OrdinalIgnoreCase)) } + new SelectListItem() { Text = S["Authenticated (no roles)"], Value = "Authenticated", Selected = String.Equals(options.SelectedRole, "Authenticated", StringComparison.OrdinalIgnoreCase) } }; // TODO Candidate for dynamic localization. - options.UserRoleFilters.AddRange(allRoles.Select(x => new SelectListItem { Text = x, Value = x, Selected = (String.Equals(options.SelectedRole, x, StringComparison.OrdinalIgnoreCase)) })); + options.UserRoleFilters.AddRange(allRoles.Select(x => new SelectListItem { Text = x, Value = x, Selected = String.Equals(options.SelectedRole, x, StringComparison.OrdinalIgnoreCase) })); // Populate options pager summary values. var startIndex = (pagerShape.Page - 1) * (pagerShape.PageSize) + 1; @@ -184,7 +184,7 @@ public async Task Index([ModelBinder(BinderType = typeof(UserFilte options.UsersCount = userEntries.Count; options.TotalItemCount = pagerShape.TotalItemCount; - var header = await _userOptionsDisplayManager.BuildEditorAsync(options, _updateModelAccessor.ModelUpdater, false, "", ""); + var header = await _userOptionsDisplayManager.BuildEditorAsync(options, _updateModelAccessor.ModelUpdater, false, String.Empty, String.Empty); var shapeViewModel = await _shapeFactory.CreateAsync("UsersAdminList", viewModel => { @@ -208,7 +208,7 @@ public async Task IndexFilterPOST(UserIndexOptions options) } // Evaluate the values provided in the form post and map them to the filter result and route values. - await _userOptionsDisplayManager.UpdateEditorAsync(options, _updateModelAccessor.ModelUpdater, false, "", ""); + await _userOptionsDisplayManager.UpdateEditorAsync(options, _updateModelAccessor.ModelUpdater, false, String.Empty, String.Empty); // The route value must always be added after the editors have updated the models. options.RouteValues.TryAdd("q", options.FilterResult.ToString()); @@ -221,14 +221,12 @@ public async Task IndexFilterPOST(UserIndexOptions options) public async Task IndexPOST(UserIndexOptions options, IEnumerable itemIds) { // Check a dummy user account to see if the current user has permission to manage it. - var authUser = new User(); - - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers, authUser)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ListUsers, new User())) { return Forbid(); } - if (itemIds?.Count() > 0) + if (itemIds != null && itemIds.Any()) { var checkedUsers = await _session.Query().Where(x => x.UserId.IsIn(itemIds)).ListAsync(); @@ -236,79 +234,61 @@ public async Task IndexPOST(UserIndexOptions options, IEnumerable< // To prevent html injection we authorize each user before performing any operations. foreach (var user in checkedUsers) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers, user)) - { - return Forbid(); - } - } + var canEditUser = await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user); + var isSameUser = user.UserId == User.FindFirstValue(ClaimTypes.NameIdentifier); - switch (options.BulkAction) - { - case UsersBulkAction.None: - break; - case UsersBulkAction.Approve: - foreach (var user in checkedUsers) - { - if (!await _userManager.IsEmailConfirmedAsync(user)) + switch (options.BulkAction) + { + case UsersBulkAction.None: break; + case UsersBulkAction.Approve: + if (canEditUser && !await _userManager.IsEmailConfirmedAsync(user)) { var token = await _userManager.GenerateEmailConfirmationTokenAsync(user); await _userManager.ConfirmEmailAsync(user, token); await _notifier.SuccessAsync(H["User {0} successfully approved.", user.UserName]); } - } - break; - case UsersBulkAction.Delete: - foreach (var user in checkedUsers) - { - if (String.Equals(user.UserId, User.FindFirstValue(ClaimTypes.NameIdentifier), StringComparison.OrdinalIgnoreCase)) + break; + case UsersBulkAction.Delete: + if (!isSameUser && await _authorizationService.AuthorizeAsync(User, CommonPermissions.DeleteUsers, user)) { - continue; + await _userManager.DeleteAsync(user); + await _notifier.SuccessAsync(H["User {0} successfully deleted.", user.UserName]); } - await _userManager.DeleteAsync(user); - await _notifier.SuccessAsync(H["User {0} successfully deleted.", user.UserName]); - } - break; - case UsersBulkAction.Disable: - foreach (var user in checkedUsers) - { - if (String.Equals(user.UserId, User.FindFirstValue(ClaimTypes.NameIdentifier), StringComparison.OrdinalIgnoreCase)) + break; + case UsersBulkAction.Disable: + if (!isSameUser && canEditUser) { - continue; + user.IsEnabled = false; + await _userManager.UpdateAsync(user); + await _notifier.SuccessAsync(H["User {0} successfully disabled.", user.UserName]); } - user.IsEnabled = false; - await _userManager.UpdateAsync(user); - await _notifier.SuccessAsync(H["User {0} successfully disabled.", user.UserName]); - } - break; - case UsersBulkAction.Enable: - foreach (var user in checkedUsers) - { - if (String.Equals(user.UserId, User.FindFirstValue(ClaimTypes.NameIdentifier), StringComparison.OrdinalIgnoreCase)) + break; + case UsersBulkAction.Enable: + if (!isSameUser && canEditUser) { - continue; + user.IsEnabled = true; + await _userManager.UpdateAsync(user); + await _notifier.SuccessAsync(H["User {0} successfully enabled.", user.UserName]); } - user.IsEnabled = true; - await _userManager.UpdateAsync(user); - await _notifier.SuccessAsync(H["User {0} successfully enabled.", user.UserName]); - } - break; - default: - throw new ArgumentOutOfRangeException(); + break; + default: + throw new ArgumentOutOfRangeException(nameof(options.BulkAction), "Invalid bulk options."); + } } - } + } return RedirectToAction(nameof(Index)); } public async Task Create() { var user = new User(); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ViewUsers, user)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } - var shape = await _userDisplayManager.BuildEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: true, "", ""); + var shape = await _userDisplayManager.BuildEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: true, String.Empty, String.Empty); return View(shape); } @@ -319,12 +299,12 @@ public async Task CreatePost([Bind(Prefix = "User.Password")] str { var user = new User(); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ViewUsers, user)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } - var shape = await _userDisplayManager.UpdateEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: true, "", ""); + var shape = await _userDisplayManager.UpdateEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: true, String.Empty, String.Empty); if (!ModelState.IsValid) { @@ -350,25 +330,24 @@ public async Task Edit(string id, string returnUrl) if (String.IsNullOrEmpty(id)) { id = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageOwnUserInformation)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditOwnUser)) { return Forbid(); } editingOwnUser = true; } - var user = await _userManager.FindByIdAsync(id) as User; - if (user == null) + if (await _userManager.FindByIdAsync(id) is not User user) { return NotFound(); } - if (!editingOwnUser && !await _authorizationService.AuthorizeAsync(User, Permissions.ViewUsers, user)) + if (!editingOwnUser && !await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } - var shape = await _userDisplayManager.BuildEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: false, "", ""); + var shape = await _userDisplayManager.BuildEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: false, String.Empty, String.Empty); ViewData["ReturnUrl"] = returnUrl; @@ -385,24 +364,23 @@ public async Task EditPost(string id, string returnUrl) { editingOwnUser = true; id = User.FindFirstValue(ClaimTypes.NameIdentifier); - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageOwnUserInformation)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditOwnUser)) { return Forbid(); } } - var user = await _userManager.FindByIdAsync(id) as User; - if (user == null) + if (await _userManager.FindByIdAsync(id) is not User user) { return NotFound(); } - if (!editingOwnUser && !await _authorizationService.AuthorizeAsync(User, Permissions.ViewUsers, user)) + if (!editingOwnUser && !await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } - var shape = await _userDisplayManager.UpdateEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: false, "", ""); + var shape = await _userDisplayManager.UpdateEditorAsync(user, updater: _updateModelAccessor.ModelUpdater, isNew: false, String.Empty, String.Empty); if (!ModelState.IsValid) { @@ -413,7 +391,7 @@ public async Task EditPost(string id, string returnUrl) foreach (var error in result.Errors) { - ModelState.AddModelError(string.Empty, error.Description); + ModelState.AddModelError(String.Empty, error.Description); } if (!ModelState.IsValid) @@ -421,7 +399,7 @@ public async Task EditPost(string id, string returnUrl) return View(shape); } - if (String.Equals(User.FindFirstValue(ClaimTypes.NameIdentifier), user.UserId, StringComparison.OrdinalIgnoreCase)) + if (User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId) { await _signInManager.RefreshSignInAsync(user); } @@ -437,28 +415,41 @@ public async Task EditPost(string id, string returnUrl) return RedirectToAction(nameof(Edit)); } - else + + if (!String.IsNullOrEmpty(returnUrl)) { - if (!String.IsNullOrEmpty(returnUrl)) - { - return this.LocalRedirect(returnUrl, true); - } + return this.LocalRedirect(returnUrl, true); + } - return RedirectToAction(nameof(Index)); + return RedirectToAction(nameof(Index)); + } + + public async Task Display(string id) + { + if (await _userManager.FindByIdAsync(id) is not User user) + { + return NotFound(); } + + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ViewUsers, user)) + { + return Forbid(); + } + + var model = await _userDisplayManager.BuildDisplayAsync(user, _updateModelAccessor.ModelUpdater, "DetailAdmin"); + + return View(model); } [HttpPost] public async Task Delete(string id) { - var user = await _userManager.FindByIdAsync(id) as User; - - if (user == null) + if (await _userManager.FindByIdAsync(id) is not User user) { return NotFound(); } - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers, user)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.DeleteUsers, user)) { return Forbid(); } @@ -486,14 +477,12 @@ public async Task Delete(string id) public async Task EditPassword(string id) { - var user = await _userManager.FindByIdAsync(id) as User; - - if (user == null) + if (await _userManager.FindByIdAsync(id) is not User user) { return NotFound(); } - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers, user)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } @@ -506,14 +495,12 @@ public async Task EditPassword(string id) [HttpPost] public async Task EditPassword(ResetPasswordViewModel model) { - var user = await _userManager.FindByEmailAsync(model.Email) as User; - - if (user == null) + if (await _userManager.FindByEmailAsync(model.Email) is not User user) { return NotFound(); } - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers, user)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { return Forbid(); } @@ -536,16 +523,14 @@ public async Task EditPassword(ResetPasswordViewModel model) [HttpPost] public async Task Unlock(string id) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers)) + if (await _userManager.FindByIdAsync(id) is not User user) { - return Forbid(); + return NotFound(); } - var user = await _userManager.FindByIdAsync(id) as User; - - if (user == null) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, user)) { - return NotFound(); + return Forbid(); } await _userManager.ResetAccessFailedCountAsync(user); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs index efd4e7c8c00..9983ebe6ed1 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/RegistrationController.cs @@ -152,7 +152,7 @@ public IActionResult RegistrationPending(string returnUrl = null) [ValidateAntiForgeryToken] public async Task SendVerificationEmail(string id) { - if (!await _authorizationService.AuthorizeAsync(User, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(User, CommonPermissions.ManageUsers)) { return Forbid(); } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs index c0f4f5a7d40..05747e29d2d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ChangeEmailSettingsDisplayDriver.cs @@ -28,7 +28,7 @@ public override async Task EditAsync(ChangeEmailSettings setting { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } @@ -43,7 +43,7 @@ public override async Task UpdateAsync(ChangeEmailSettings secti { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs index b795489e9e8..4d07548afa7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginSettingsDisplayDriver.cs @@ -26,7 +26,7 @@ public override async Task EditAsync(LoginSettings settings, Bui { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } @@ -38,14 +38,14 @@ public override async Task EditAsync(LoginSettings settings, Bui model.DisableLocalLogin = settings.DisableLocalLogin; model.UseScriptToSyncRoles = settings.UseScriptToSyncRoles; model.SyncRolesScript = settings.SyncRolesScript; + model.AllowChangingEmail = settings.AllowChangingEmail; + model.AllowChangingUsername = settings.AllowChangingUsername; }).Location("Content:5").OnGroup(GroupId); } public override async Task UpdateAsync(LoginSettings section, BuildEditorContext context) { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext?.User, CommonPermissions.ManageUsers)) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs index 2a686310cf3..aa604795de3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/RegistrationSettingsDisplayDriver.cs @@ -28,7 +28,7 @@ public override async Task EditAsync(RegistrationSettings settin { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } @@ -51,7 +51,7 @@ public override async Task UpdateAsync(RegistrationSettings sect { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs index 43fe1f732b9..a465d7e0eb3 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ResetPasswordSettingsDisplayDriver.cs @@ -29,7 +29,7 @@ public override async Task EditAsync(ResetPasswordSettings setti { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } @@ -45,7 +45,7 @@ public override async Task UpdateAsync(ResetPasswordSettings sec { var user = _httpContextAccessor.HttpContext?.User; - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageUsers)) + if (!await _authorizationService.AuthorizeAsync(user, CommonPermissions.ManageUsers)) { return null; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs index 3216f9d9b43..416ebbc60bf 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserDisplayDriver.cs @@ -7,16 +7,16 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Notify; using OrchardCore.DisplayManagement.Views; using OrchardCore.Modules; +using OrchardCore.Mvc.ModelBinding; using OrchardCore.Users.Handlers; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; -using OrchardCore.Mvc.ModelBinding; -using Microsoft.Extensions.Localization; namespace OrchardCore.Users.Drivers { @@ -27,7 +27,7 @@ public class UserDisplayDriver : DisplayDriver private readonly IHttpContextAccessor _httpContextAccessor; private readonly INotifier _notifier; private readonly IAuthorizationService _authorizationService; - private IEnumerable _userEventHandlers; + private readonly IEnumerable _userEventHandlers; private readonly ILogger _logger; private readonly IHtmlLocalizer H; private readonly IStringLocalizer S; @@ -56,32 +56,36 @@ public override IDisplayResult Display(User user) { return Combine( Initialize("UserFields", model => model.User = user).Location("SummaryAdmin", "Header:1"), + Initialize("UserInfo", model => model.User = user).Location("DetailAdmin", "Content:5"), Initialize("UserButtons", model => model.User = user).Location("SummaryAdmin", "Actions:1") ); } - public override Task EditAsync(User user, BuildEditorContext context) + public override async Task EditAsync(User user, BuildEditorContext context) { - return Task.FromResult(Initialize("UserFields_Edit", async model => - { - model.EmailConfirmed = user.EmailConfirmed; - model.IsEnabled = user.IsEnabled; - model.IsNewRequest = context.IsNew; - // The current user cannot disable themselves, nor can a user without permission to manage this user disable them. - model.IsEditingDisabled = !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageUsers, user) || - String.Equals(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), user.UserId, StringComparison.OrdinalIgnoreCase); - }) - .Location("Content:1.5") - .RenderWhen(() => _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ViewUsers, user))); + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditUsers, user)) + { + return null; + } + + return Initialize("UserFields_Edit", model => + { + model.EmailConfirmed = user.EmailConfirmed; + model.IsEnabled = user.IsEnabled; + model.IsNewRequest = context.IsNew; + // The current user cannot disable themselves, nor can a user without permission to manage this user disable them. + model.IsEditingDisabled = IsCurrentUser(user); + }) + .Location("Content:1.5"); } public override async Task UpdateAsync(User user, UpdateEditorContext context) { // To prevent html injection when updating the user must meet all authorization requirements. - if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageUsers, user)) + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditUsers, user)) { // When the user is only editing their profile never update this part of the user. - return Edit(user); + return await EditAsync(user, context); } var model = new EditUserViewModel(); @@ -90,7 +94,7 @@ public override async Task UpdateAsync(User user, UpdateEditorCo if (context.IsNew) { - if (string.IsNullOrWhiteSpace(model.Password)) + if (String.IsNullOrWhiteSpace(model.Password)) { context.Updater.ModelState.AddModelError(Prefix, nameof(model.Password), S["A password is required"]); } @@ -106,15 +110,18 @@ public override async Task UpdateAsync(User user, UpdateEditorCo return await EditAsync(user, context); } - var isEditingDisabled = !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageUsers, user) || - String.Equals(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), user.UserId, StringComparison.OrdinalIgnoreCase); + var isEditingDisabled = IsCurrentUser(user); if (!isEditingDisabled && !model.IsEnabled && user.IsEnabled) { - var usersOfAdminRole = (await _userManager.GetUsersInRoleAsync(AdministratorRole)).Cast(); - if (usersOfAdminRole.Count() == 1 && String.Equals(user.UserId, usersOfAdminRole.First().UserId, StringComparison.OrdinalIgnoreCase)) + var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(AdministratorRole)) + .Cast() + .Where(user => user.IsEnabled) + .ToList(); + + if (enabledUsersOfAdminRole.Count == 1 && user.UserId == enabledUsersOfAdminRole.First().UserId) { - await _notifier.WarningAsync(H["Cannot disable the only administrator."]); + await _notifier.WarningAsync(H["Cannot disable the only enabled administrator."]); } else { @@ -145,5 +152,10 @@ public override async Task UpdateAsync(User user, UpdateEditorCo return await EditAsync(user, context); } + + private bool IsCurrentUser(User user) + { + return _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId; + } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserInformationDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserInformationDisplayDriver.cs index be17783ca2d..3665b562869 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserInformationDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserInformationDisplayDriver.cs @@ -1,10 +1,11 @@ -using System; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Entities; +using OrchardCore.Settings; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; @@ -14,73 +15,103 @@ public class UserInformationDisplayDriver : DisplayDriver { private readonly IHttpContextAccessor _httpContextAccessor; private readonly IAuthorizationService _authorizationService; + private readonly ISiteService _siteService; public UserInformationDisplayDriver( IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService) + IAuthorizationService authorizationService, + ISiteService siteService) { _httpContextAccessor = httpContextAccessor; _authorizationService = authorizationService; + _siteService = siteService; } - public override IDisplayResult Edit(User user) + public override async Task EditAsync(User user, BuildEditorContext context) { - return Initialize("UserInformationFields_Edit", async model => + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditUsers, user)) { - model.UserName = user.UserName; - model.Email = user.Email; - model.IsEditingDisabled = !await AuthorizeUpdateAsync(user); - }) - .Location("Content:1") - .RenderWhen(() => AuthorizeEditAsync(user)); + return null; + } + + var site = await _siteService.GetSiteSettingsAsync(); + var settings = site.As(); + + return Combine( + Initialize("UserName_Edit", async model => + { + model.UserName = user.UserName; + + model.AllowEditing = context.IsNew || (settings.AllowChangingUsername && await CanEditUserInfoAsync(user)); + + }).Location("Content:1"), + + Initialize("UserEmail_Edit", async model => + { + model.Email = user.Email; + + model.AllowEditing = context.IsNew || (settings.AllowChangingEmail && await CanEditUserInfoAsync(user)); + + }).Location("Content:1.3") + ); } + public override async Task UpdateAsync(User user, UpdateEditorContext context) { - if (!await AuthorizeUpdateAsync(user)) + if (!await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditUsers, user)) { - return Edit(user); + return null; } - var model = new EditUserInformationViewModel(); + var userNameModel = new EditUserNameViewModel(); + var emailModel = new EditUserEmailViewModel(); - if (await context.Updater.TryUpdateModelAsync(model, Prefix)) - { - // Do not use the user manager to set these values, or validate them here, as they will validate at the incorrect time. - // After this driver runs the IUserService.UpdateAsync or IUserService.CreateAsync method will - // validate the user and provide the correct error messages based on the entire user objects values. + // Do not use the user manager to set these values, or validate them here, as they will validate at the incorrect time. + // After this driver runs the IUserService.UpdateAsync or IUserService.CreateAsync method will + // validate the user and provide the correct error messages based on the entire user objects values. - // Custom properties should still be validated in the driver. + // Custom properties should still be validated in the driver. - user.UserName = model.UserName; - user.Email = model.Email; + if (context.IsNew) + { + if (await context.Updater.TryUpdateModelAsync(userNameModel, Prefix)) + { + user.UserName = userNameModel.UserName; + } + + if (await context.Updater.TryUpdateModelAsync(emailModel, Prefix)) + { + user.Email = emailModel.Email; + } } + else + { + var site = await _siteService.GetSiteSettingsAsync(); + var settings = site.As(); - return Edit(user); - } + if (settings.AllowChangingUsername && await CanEditUserInfoAsync(user) && await context.Updater.TryUpdateModelAsync(userNameModel, Prefix)) + { + user.UserName = userNameModel.UserName; + } - private Task AuthorizeUpdateAsync(User user) - { - // When the current user matches this user we can ask for ManageOwnUserInformation - if (String.Equals(user.UserId, _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), StringComparison.OrdinalIgnoreCase)) - { - return _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageOwnUserInformation); + if (settings.AllowChangingEmail && await CanEditUserInfoAsync(user) && await context.Updater.TryUpdateModelAsync(emailModel, Prefix)) + { + user.Email = emailModel.Email; + } } - // Otherwise we require permission to manage this users information. - return _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageUsers, user); + return await EditAsync(user, context); } - private Task AuthorizeEditAsync(User user) + private async Task CanEditUserInfoAsync(User user) { - // When the current user matches this user we can ask for ManageOwnUserInformation - if (String.Equals(user.UserId, _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), StringComparison.OrdinalIgnoreCase)) - { - return _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ManageOwnUserInformation); - } + return !IsCurrentUser(user) || await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditOwnUser); + } - // Otherwise we require permission to manage this users information. - return _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ViewUsers, user); + private bool IsCurrentUser(User user) + { + return _httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserRoleDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserRoleDisplayDriver.cs index b713cf5bfa6..e82c20343a5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserRoleDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/UserRoleDisplayDriver.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -11,6 +10,7 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Notify; using OrchardCore.DisplayManagement.Views; +using OrchardCore.Security; using OrchardCore.Security.Services; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; @@ -19,7 +19,7 @@ namespace OrchardCore.Users.Drivers { public class UserRoleDisplayDriver : DisplayDriver { - private const string AdministratorRole = "Administrator"; + private const string _administratorRole = "Administrator"; private readonly UserManager _userManager; private readonly IRoleService _roleService; @@ -47,44 +47,51 @@ public UserRoleDisplayDriver( H = htmlLocalizer; } + public override IDisplayResult Display(User user) + { + return Combine( + Initialize("UserRolesMeta", model => model.User = user) + .Location("SummaryAdmin", "Description"), + + Initialize("UserRoles", model => model.User = user) + .Location("DetailAdmin", "Content:10") + ); + } + public override IDisplayResult Edit(User user) { // This view is always rendered, however there will be no editable roles if the user does not have permission to edit them. return Initialize("UserRoleFields_Edit", async model => { - // The current user can only view their roles if they have list users, to prevent listing roles when managing their own profile. - if (String.Equals(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), user.UserId, StringComparison.OrdinalIgnoreCase) && - !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, Permissions.ViewUsers)) + // The current user can only view their roles if they have assign role, to prevent listing roles when managing their own profile. + if (_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId + && !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.AssignRoleToUsers)) { return; } - var roleNames = await GetRoleNamesAsync(); + var roles = await GetRoleAsync(); // When a user is in a role that the current user cannot manage the role is shown but selection is disabled. - var authorizedRoleNames = await GetAuthorizedRoleNamesAsync(roleNames); - var userRoleNames = await _userRoleStore.GetRolesAsync(user, default(CancellationToken)); + var authorizedRoleNames = await GetAccessibleRoleNamesAsync(roles); + var userRoleNames = await _userRoleStore.GetRolesAsync(user, default); var roleEntries = new List(); - foreach (var roleName in roleNames) + foreach (var roleName in authorizedRoleNames) { var roleEntry = new RoleEntry { Role = roleName, - IsSelected = userRoleNames.Contains(roleName, StringComparer.OrdinalIgnoreCase) + IsSelected = userRoleNames.Contains(roleName, StringComparer.OrdinalIgnoreCase), }; - if (!authorizedRoleNames.Contains(roleName, StringComparer.OrdinalIgnoreCase)) - { - roleEntry.IsEditingDisabled = true; - } - roleEntries.Add(roleEntry); } model.Roles = roleEntries.ToArray(); }) - .Location("Content:1.10"); + .Location("Content:1.10") + .RenderWhen(async () => await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.EditUsers, user)); } public override async Task UpdateAsync(User user, UpdateEditorContext context) @@ -92,67 +99,75 @@ public override async Task UpdateAsync(User user, UpdateEditorCo var model = new EditUserRoleViewModel(); // The current user cannot alter their own roles. This prevents them removing access to the site for themselves. - if (String.Equals(_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier), user.UserId, StringComparison.OrdinalIgnoreCase)) + if (_httpContextAccessor.HttpContext.User.FindFirstValue(ClaimTypes.NameIdentifier) == user.UserId + && !await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, StandardPermissions.SiteOwner)) { + await _notifier.WarningAsync(H["Cannot update your own roles."]); + return Edit(user); } if (await context.Updater.TryUpdateModelAsync(model, Prefix)) { + var roles = await GetRoleAsync(); // Authorize each role in the model to prevent html injection. - var authorizedRoleNames = await GetAuthorizedRoleNamesAsync(model.Roles.Select(x => x.Role)); - var userRoleNames = await _userRoleStore.GetRolesAsync(user, default(CancellationToken)); + var accessibleRoleNames = await GetAccessibleRoleNamesAsync(roles); + var currentUserRoleNames = await _userRoleStore.GetRolesAsync(user, default); - var authorizedSelectedRoleNames = await GetAuthorizedRoleNamesAsync(model.Roles.Where(x => x.IsSelected).Select(x => x.Role)); + var selectedRoleNames = model.Roles.Where(x => x.IsSelected).Select(x => x.Role); + var selectedRoles = roles.Where(x => selectedRoleNames.Contains(x.RoleName, StringComparer.OrdinalIgnoreCase)); + var accessibleAndSelectedRoleNames = await GetAccessibleRoleNamesAsync(selectedRoles); if (context.IsNew) { // Only add authorized new roles. - foreach (var role in authorizedSelectedRoleNames) + foreach (var role in accessibleAndSelectedRoleNames) { - await _userRoleStore.AddToRoleAsync(user, _userManager.NormalizeName(role), default(CancellationToken)); + await _userRoleStore.AddToRoleAsync(user, _userManager.NormalizeName(role), default); } } else { - // Remove roles in two steps to prevent an iteration on a modified collection + // Remove roles in two steps to prevent an iteration on a modified collection. var rolesToRemove = new List(); - foreach (var role in userRoleNames) + foreach (var role in currentUserRoleNames) { // When the user has permission to manage the role and it is no longer selected the role can be removed. - if (authorizedRoleNames.Contains(role, StringComparer.OrdinalIgnoreCase) && !authorizedSelectedRoleNames.Contains(role, StringComparer.OrdinalIgnoreCase)) + if (accessibleRoleNames.Contains(role, StringComparer.OrdinalIgnoreCase) + && !accessibleAndSelectedRoleNames.Contains(role, StringComparer.OrdinalIgnoreCase)) { rolesToRemove.Add(role); } } + foreach (var role in rolesToRemove) { - if (String.Equals(role, AdministratorRole, StringComparison.OrdinalIgnoreCase)) + if (String.Equals(role, _administratorRole, StringComparison.OrdinalIgnoreCase)) { - var usersOfAdminRole = (await _userManager.GetUsersInRoleAsync(AdministratorRole)).Cast(); - // Make sure we always have at least one administrator account - if (usersOfAdminRole.Count() == 1 && String.Equals(user.UserId, usersOfAdminRole.First().UserId, StringComparison.OrdinalIgnoreCase)) + var enabledUsersOfAdminRole = (await _userManager.GetUsersInRoleAsync(_administratorRole)) + .Cast() + .Where(user => user.IsEnabled) + .ToList(); + + // Make sure we always have at least one enabled administrator account. + if (enabledUsersOfAdminRole.Count == 1 && user.UserId == enabledUsersOfAdminRole.First().UserId) { - await _notifier.WarningAsync(H["Cannot remove administrator role from the only administrator."]); + await _notifier.WarningAsync(H[$"Cannot remove {_administratorRole} role from the only enabled administrator."]); + continue; } - else - { - await _userRoleStore.RemoveFromRoleAsync(user, _userManager.NormalizeName(role), default(CancellationToken)); - } - } - else - { - await _userRoleStore.RemoveFromRoleAsync(user, _userManager.NormalizeName(role), default(CancellationToken)); } + + await _userRoleStore.RemoveFromRoleAsync(user, _userManager.NormalizeName(role), default); } - // Add new roles - foreach (var role in authorizedSelectedRoleNames) + // Add new roles. + foreach (var role in accessibleAndSelectedRoleNames) { - if (!await _userRoleStore.IsInRoleAsync(user, _userManager.NormalizeName(role), default(CancellationToken))) + var normalizedName = _userManager.NormalizeName(role); + if (!await _userRoleStore.IsInRoleAsync(user, normalizedName, default)) { - await _userRoleStore.AddToRoleAsync(user, _userManager.NormalizeName(role), default(CancellationToken)); + await _userRoleStore.AddToRoleAsync(user, normalizedName, default); } } } @@ -161,20 +176,22 @@ public override async Task UpdateAsync(User user, UpdateEditorCo return Edit(user); } - private async Task> GetRoleNamesAsync() + private async Task> GetRoleAsync() { - var roleNames = await _roleService.GetRoleNamesAsync(); - return roleNames.Except(new[] { "Anonymous", "Authenticated" }, StringComparer.OrdinalIgnoreCase); + var roles = await _roleService.GetRolesAsync(); + + return roles.Where(role => !RoleHelper.SystemRoleNames.Contains(role.RoleName)); } - private async Task> GetAuthorizedRoleNamesAsync(IEnumerable roleNames) + private async Task> GetAccessibleRoleNamesAsync(IEnumerable roles) { var authorizedRoleNames = new List(); - foreach (var roleName in roleNames) + + foreach (var role in roles) { - if (await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, OrchardCore.Roles.CommonPermissions.CreatePermissionForAssignRole(roleName))) + if (await _authorizationService.AuthorizeAsync(_httpContextAccessor.HttpContext.User, CommonPermissions.AssignRoleToUsers, role)) { - authorizedRoleNames.Add(roleName); + authorizedRoleNames.Add(role.RoleName); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Models/LoginSettings.cs b/src/OrchardCore.Modules/OrchardCore.Users/Models/LoginSettings.cs index 5b9f5b50fad..9547ea1456a 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Models/LoginSettings.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Models/LoginSettings.cs @@ -11,5 +11,9 @@ public class LoginSettings public bool UseScriptToSyncRoles { get; set; } public string SyncRolesScript { get; set; } + + public bool AllowChangingUsername { get; set; } + + public bool AllowChangingEmail { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Permissions.cs b/src/OrchardCore.Modules/OrchardCore.Users/Permissions.cs index 555022f9c94..351b74b7591 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Permissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Permissions.cs @@ -8,16 +8,18 @@ public class Permissions : IPermissionProvider { public static readonly Permission ManageUsers = CommonPermissions.ManageUsers; public static readonly Permission ViewUsers = CommonPermissions.ViewUsers; - - public static readonly Permission ManageOwnUserInformation = new Permission("ManageOwnUserInformation", "Manage own user information", new Permission[] { ManageUsers }); + public static readonly Permission ManageOwnUserInformation = CommonPermissions.EditOwnUser; public Task> GetPermissionsAsync() { return Task.FromResult>(new List { - ManageUsers, - ManageOwnUserInformation, - ViewUsers, + CommonPermissions.ManageUsers, + CommonPermissions.ViewUsers, + CommonPermissions.EditOwnUser, + CommonPermissions.ListUsers, + CommonPermissions.EditUsers, + CommonPermissions.DeleteUsers, }); } @@ -26,23 +28,30 @@ public IEnumerable GetDefaultStereotypes() return new[] { new PermissionStereotype { Name = "Administrator", - Permissions = new[] { ManageUsers } + Permissions = new[] { + CommonPermissions.ManageUsers, + CommonPermissions.ViewUsers, + CommonPermissions.EditOwnUser, + CommonPermissions.ListUsers, + CommonPermissions.EditUsers, + CommonPermissions.DeleteUsers, + } }, new PermissionStereotype { Name = "Editor", - Permissions = new[] { ManageOwnUserInformation } + Permissions = new[] { CommonPermissions.EditOwnUser } }, new PermissionStereotype { Name = "Moderator", - Permissions = new[] { ManageOwnUserInformation } + Permissions = new[] { CommonPermissions.EditOwnUser } }, new PermissionStereotype { Name = "Contributor", - Permissions = new[] { ManageOwnUserInformation } + Permissions = new[] { CommonPermissions.EditOwnUser } }, new PermissionStereotype { Name = "Author", - Permissions = new[] { ManageOwnUserInformation } + Permissions = new[] { CommonPermissions.EditOwnUser } } }; } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListFilterProvider.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListFilterProvider.cs index ce0e6791ba0..e12748f5d0c 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListFilterProvider.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListFilterProvider.cs @@ -1,7 +1,10 @@ using System; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Security.Services; using OrchardCore.Users.Indexes; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; @@ -43,15 +46,12 @@ public void Build(QueryEngineBuilder builder) }) .MapFrom((model) => { - switch (model.Filter) + return model.Filter switch { - case UsersFilter.Enabled: - return (true, model.Filter.ToString()); - case UsersFilter.Disabled: - return (true, model.Filter.ToString()); - } - - return (false, String.Empty); + UsersFilter.Enabled => (true, model.Filter.ToString()), + UsersFilter.Disabled => (true, model.Filter.ToString()), + _ => (false, String.Empty) + }; }) ) .WithNamedTerm("sort", builder => builder diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListQueryService.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListQueryService.cs index f5c7b1a753a..d5b90eaf707 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListQueryService.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/DefaultUsersAdminListQueryService.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Modules; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; using YesSql; @@ -13,15 +15,18 @@ public class DefaultUsersAdminListQueryService : IUsersAdminListQueryService private readonly ISession _session; private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; + private readonly IEnumerable _usersAdminListFilters; public DefaultUsersAdminListQueryService( ISession session, IServiceProvider serviceProvider, - ILogger logger) + ILogger logger, + IEnumerable usersAdminListFilters) { _session = session; _serviceProvider = serviceProvider; _logger = logger; + _usersAdminListFilters = usersAdminListFilters; } public async Task> QueryAsync(UserIndexOptions options, IUpdateModel updater) @@ -31,6 +36,8 @@ public async Task> QueryAsync(UserIndexOptions options, IUpdateMode query = await options.FilterResult.ExecuteAsync(new UserQueryContext(_serviceProvider, query)); + await _usersAdminListFilters.InvokeAsync((filter, model, query, updater) => filter.FilterAsync(model, query, updater), options, query, updater, _logger); + return query; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleAuthorizationHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleAuthorizationHandler.cs new file mode 100644 index 00000000000..ad094766b23 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RoleAuthorizationHandler.cs @@ -0,0 +1,127 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Security; +using OrchardCore.Security.Permissions; +using OrchardCore.Security.Services; +using OrchardCore.Users.Models; + +namespace OrchardCore.Users.Services; + +public class RoleAuthorizationHandler : AuthorizationHandler +{ + private readonly IServiceProvider _serviceProvider; + + private IAuthorizationService _authorizationService; + private IRoleService _roleService; + + public RoleAuthorizationHandler(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) + { + if (context.HasSucceeded) + { + // This handler is not revoking any pre-existing grants. + return; + } + + if (context.Resource == null) + { + return; + } + + // Lazy load to prevent circular dependencies + _authorizationService ??= _serviceProvider?.GetService(); + _roleService ??= _serviceProvider?.GetService(); + + if (context.Resource is IRole role) + { + var variantPermission = GetPermissionVariation(requirement.Permission, role.RoleName); + + if (variantPermission != null && await _authorizationService.AuthorizeAsync(context.User, variantPermission)) + { + context.Succeed(requirement); + + return; + } + } + + if (context.Resource is User user) + { + var currentUserId = context.User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (requirement.Permission.Name == CommonPermissions.EditUsers.Name + && user.UserId == currentUserId + && await _authorizationService.AuthorizeAsync(context.User, CommonPermissions.EditOwnUser)) + { + context.Succeed(requirement); + + return; + } + + if (await _authorizationService.AuthorizeAsync(context.User, requirement.Permission)) + { + context.Succeed(requirement); + + return; + } + + var roleNames = user.RoleNames ?? Enumerable.Empty(); + + if (!roleNames.Any()) + { + // When the user is in no roles, we check to see if the current user can manage any roles. + roleNames = (await _roleService.GetRoleNamesAsync()).Where(roleName => !RoleHelper.SystemRoleNames.Contains(roleName)); + } + + // Check every role to see if the current user has permission to at least one role. + foreach (var roleName in roleNames) + { + var variantPermission = GetPermissionVariation(requirement.Permission, roleName); + + if (variantPermission != null && await _authorizationService.AuthorizeAsync(context.User, variantPermission)) + { + context.Succeed(requirement); + + return; + } + } + } + } + + private Permission GetPermissionVariation(Permission permission, string roleName) + { + if (permission.Name == CommonPermissions.ListUsers.Name) + { + return CommonPermissions.CreateListUsersInRolePermission(roleName); + } + + if (permission.Name == CommonPermissions.EditUsers.Name) + { + return CommonPermissions.CreateEditUsersInRolePermission(roleName); + } + + if (permission.Name == CommonPermissions.DeleteUsers.Name) + { + return CommonPermissions.CreateDeleteUsersInRolePermission(roleName); + } + + if (permission.Name == CommonPermissions.AssignRoleToUsers.Name) + { + return CommonPermissions.CreateAssignRoleToUsersPermission(roleName); + } + + if (permission.Name == CommonPermissions.ManageUsers.Name) + { + return CommonPermissions.CreatePermissionForManageUsersInRole(roleName); + } + + return null; + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/RolesAdminListFilterProvider.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/RolesAdminListFilterProvider.cs new file mode 100644 index 00000000000..39548fe9ab3 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Services/RolesAdminListFilterProvider.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Security.Services; +using OrchardCore.Users.Indexes; +using OrchardCore.Users.Models; +using YesSql.Filters.Query; +using YesSql.Services; + +namespace OrchardCore.Users.Services; + +public class RolesAdminListFilterProvider : IUsersAdminListFilterProvider +{ + public void Build(QueryEngineBuilder builder) + { + builder.WithNamedTerm("role-restriction", builder => builder + .OneCondition(async (contentType, query, ctx) => + { + var context = (UserQueryContext)ctx; + + var httpContextAccessor = context.ServiceProvider.GetRequiredService(); + var authorizationService = context.ServiceProvider.GetRequiredService(); + var roleService = context.ServiceProvider.GetRequiredService(); + + var user = httpContextAccessor.HttpContext?.User; + + if (user != null && !await authorizationService.AuthorizeAsync(user, CommonPermissions.ListUsers)) + { + // At this point the user cannot see all users, so lets see what role does he have access too and filter by them. + var accessibleRoles = await roleService.GetAccessibleRoleNamesAsync(authorizationService, user, CommonPermissions.ListUsers); + + query.With(index => index.RoleName.IsIn(accessibleRoles)); + } + + return query; + }) + .AlwaysRun() + ); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Services/UserAuthorizationHandler.cs b/src/OrchardCore.Modules/OrchardCore.Users/Services/UserAuthorizationHandler.cs deleted file mode 100644 index bf75b19789c..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Users/Services/UserAuthorizationHandler.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; -using Microsoft.Extensions.DependencyInjection; -using OrchardCore.Security; -using OrchardCore.Security.Services; - -namespace OrchardCore.Users.Services -{ - /// - /// This provides authorization when the permission request is ManageUsers and the current authenticated User - /// has permission to edit any of the roles the user being managed is a member of. - /// Callers should supply a resource - /// - /// - /// This handler will not be triggered when only requesting ManagerUsers permission with supplying an . - /// - public class UserAuthorizationHandler : AuthorizationHandler - { - private readonly IServiceProvider _serviceProvider; - private readonly UserManager _userManager; - private readonly IRoleService _roleService; - - public UserAuthorizationHandler( - IServiceProvider serviceProvider, - UserManager userManager, - IRoleService roleService) - { - _serviceProvider = serviceProvider; - _userManager = userManager; - _roleService = roleService; - } - - protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) - { - if (context.HasSucceeded) - { - // This handler is not revoking any pre-existing grants. - return; - } - - if (context.Resource == null) - { - return; - } - - if (!String.Equals(requirement.Permission.Name, Permissions.ManageUsers.Name, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - var user = context.Resource as IUser; - - if (user == null) - { - return; - } - - // Lazy load to prevent circular dependencies - var authorizationService = _serviceProvider.GetService(); - - var userRoleNames = await _userManager.GetRolesAsync(user); - - if (userRoleNames.Any()) - { - // When the user has roles we check to see if any of these roles are not authorized before succeeding. - if (await AuthorizeRolesAsync(authorizationService, context.User, userRoleNames)) - { - context.Succeed(requirement); - } - } - else - { - var roleNames = await _roleService.GetRoleNamesAsync(); - // When the user is in no roles, we check to see if the current user can manage any roles. - if (await HasAuthorizationToManageAnyRoleAsync(authorizationService, context.User, roleNames)) - { - context.Succeed(requirement); - } - } - } - - private async Task AuthorizeRolesAsync(IAuthorizationService authorizationService, ClaimsPrincipal user, IEnumerable roleNames) - { - foreach (var roleName in roleNames) - { - if (!await authorizationService.AuthorizeAsync(user, CommonPermissions.CreatePermissionForManageUsersInRole(roleName))) - { - return false; - } - } - - return true; - } - - private async Task HasAuthorizationToManageAnyRoleAsync(IAuthorizationService authorizationService, ClaimsPrincipal user, IEnumerable roleNames) - { - foreach (var roleName in roleNames) - { - if (await authorizationService.AuthorizeAsync(user, CommonPermissions.CreatePermissionForManageUsersInRole(roleName))) - { - return true; - } - } - - return false; - } - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index 1fb150b4db3..f021b5497e0 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OrchardCore.Admin; +using OrchardCore.Data; using OrchardCore.Data.Migration; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Theming; @@ -36,6 +37,7 @@ using OrchardCore.Users.Controllers; using OrchardCore.Users.Drivers; using OrchardCore.Users.Handlers; +using OrchardCore.Users.Indexes; using OrchardCore.Users.Liquid; using OrchardCore.Users.Models; using OrchardCore.Users.Services; @@ -133,7 +135,12 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde pattern: _adminOptions.AdminUrlPrefix + "/Users/Unlock/{id}", defaults: new { controller = adminControllerName, action = nameof(AdminController.Unlock) } ); - + routes.MapAreaControllerRoute( + name: "UsersDisplay", + areaName: "OrchardCore.Users", + pattern: _adminOptions.AdminUrlPrefix + "/Users/Display/{id}", + defaults: new { controller = adminControllerName, action = nameof(AdminController.Display) } + ); builder.UseAuthorization(); } @@ -189,7 +196,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -198,7 +204,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped, LoginSettingsDisplayDriver>(); services.AddScoped, UserDisplayDriver>(); - services.AddScoped, UserRoleDisplayDriver>(); services.AddScoped, UserInformationDisplayDriver>(); services.AddScoped, UserButtonsDisplayDriver>(); @@ -230,12 +235,16 @@ public override void ConfigureServices(IServiceCollection services) } [RequireFeatures("OrchardCore.Roles")] - public class RoleStartup : StartupBase + public class RolesStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.AddScoped(); + services.AddScoped(); + services.AddIndexProvider(); + services.AddScoped, UserRoleDisplayDriver>(); + services.AddScoped(); services.AddScoped(); + services.AddSingleton(); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/UserRolePermissions.cs b/src/OrchardCore.Modules/OrchardCore.Users/UserRolePermissions.cs index 651fdb838a1..5929d19a1f8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/UserRolePermissions.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/UserRolePermissions.cs @@ -1,28 +1,53 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using OrchardCore.Security.Permissions; using OrchardCore.Security.Services; -namespace OrchardCore.Users +namespace OrchardCore.Users; + +public class UserRolePermissions : IPermissionProvider { - public class UserRolePermissions : IPermissionProvider + private readonly IRoleService _roleService; + + public UserRolePermissions(IRoleService roleService) + { + _roleService = roleService; + } + + public async Task> GetPermissionsAsync() { - private readonly IRoleService _roleService; + var roleNames = (await _roleService.GetRoleNamesAsync()) + .Where(roleName => !RoleHelper.SystemRoleNames.Contains(roleName)) + .OrderBy(roleName => roleName); - public UserRolePermissions(IRoleService roleService) + var list = new List() { - _roleService = roleService; - } + CommonPermissions.AssignRoleToUsers, + }; - public async Task> GetPermissionsAsync() + foreach (var roleName in roleNames) { - return (await _roleService.GetRoleNamesAsync()) - .Except(new[] { "Anonymous", "Authenticated" }, StringComparer.OrdinalIgnoreCase) - .Select(role => CommonPermissions.CreatePermissionForManageUsersInRole(role)); + list.Add(CommonPermissions.CreateListUsersInRolePermission(roleName)); + list.Add(CommonPermissions.CreateEditUsersInRolePermission(roleName)); + list.Add(CommonPermissions.CreateDeleteUsersInRolePermission(roleName)); + list.Add(CommonPermissions.CreateAssignRoleToUsersPermission(roleName)); + list.Add(CommonPermissions.CreatePermissionForManageUsersInRole(roleName)); } - public IEnumerable GetDefaultStereotypes() => Enumerable.Empty(); + return list; + } + + public IEnumerable GetDefaultStereotypes() + { + return new[] { + new PermissionStereotype + { + Name = "Administrator", + Permissions = new[] { + CommonPermissions.AssignRoleToUsers, + } + } + }; } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserEmailViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserEmailViewModel.cs new file mode 100644 index 00000000000..ae1e83b6545 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserEmailViewModel.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Users.ViewModels; + +public class EditUserEmailViewModel +{ + [Required(ErrorMessage = "Email is required.")] + [Email.EmailAddress(ErrorMessage = "Invalid Email.")] + public string Email { get; set; } + + [BindNever] + public bool AllowEditing { get; set; } +} + diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserNameViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserNameViewModel.cs new file mode 100644 index 00000000000..0cfad2eaf04 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserNameViewModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace OrchardCore.Users.ViewModels; + +public class EditUserNameViewModel +{ + [Required] + public string UserName { get; set; } + + [BindNever] + public bool AllowEditing { get; set; } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserRoleViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserRoleViewModel.cs index de009146558..06f74564e9d 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserRoleViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/EditUserRoleViewModel.cs @@ -1,5 +1,4 @@ using System; -using Microsoft.AspNetCore.Mvc.ModelBinding; namespace OrchardCore.Users.ViewModels { @@ -11,9 +10,7 @@ public class EditUserRoleViewModel public class RoleEntry { public string Role { get; set; } - public bool IsSelected { get; set; } - [BindNever] - public bool IsEditingDisabled { get; set; } + public bool IsSelected { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Display.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Display.cshtml new file mode 100644 index 00000000000..32eceece3e6 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Display.cshtml @@ -0,0 +1 @@ +@await DisplayAsync(Model) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Index.cshtml index 6c001d65908..6a1a0745287 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Index.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/Admin/Index.cshtml @@ -1,8 +1,5 @@ @model UsersIndexViewModel -@{ - int startIndex = (Model.Pager.Page - 1) * (Model.Pager.PageSize) + 1; - int endIndex = startIndex + Model.Users.Count - 1; -} +

@RenderTitleSegments(T["Users"])

diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml index 1903ee9c3ee..27216f43eb5 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/LoginSettings.Edit.cshtml @@ -8,6 +8,20 @@ +
+
+ + +
+
+ +
+
+ + +
+
+
@@ -102,7 +116,7 @@ ' });\n' + ' default:\n' + ' log("Warning", "Provider " + loginProvider + " was not handled");\n' + - ' break;\n' + + ' break;\n' + '}\n' + '*/\n' ); @@ -117,23 +131,23 @@ editor.options.readOnly = "nocursor"; } } -$(function () { - var textArea = document.getElementById('@Html.IdFor(x => x.SyncRolesScript)'); - var syncRolesCheckbox = document.getElementById('@Html.IdFor(x => x.UseScriptToSyncRoles)'); - if (textArea == null) { - return; - } - var editor = CodeMirror.fromTextArea(textArea, { - autoRefresh: true, - lineNumbers: true, - styleActiveLine: true, - matchBrackets: true, - autoCloseTags: true, - mode: "javascript" + $(function () { + var textArea = document.getElementById('@Html.IdFor(x => x.SyncRolesScript)'); + var syncRolesCheckbox = document.getElementById('@Html.IdFor(x => x.UseScriptToSyncRoles)'); + if (textArea == null) { + return; + } + var editor = CodeMirror.fromTextArea(textArea, { + autoRefresh: true, + lineNumbers: true, + styleActiveLine: true, + matchBrackets: true, + autoCloseTags: true, + mode: "javascript" + }); + $('#@Html.IdFor(x => x.SyncRolesScript)').data('editor', editor); + resetScript(editor.doc.getValue() != ''); + syncRolesCheckbox.addEventListener("change", toggleEditorState); + toggleEditorState(); }); - $('#@Html.IdFor(x => x.SyncRolesScript)').data('editor', editor); - resetScript(editor.doc.getValue() != ''); - syncRolesCheckbox.addEventListener("change", toggleEditorState); - toggleEditorState(); -}); diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/NavigationItemText-UserProfileSettings.Id.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/NavigationItemText-UserProfileSettings.Id.cshtml new file mode 100644 index 00000000000..081b06a99fd --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/NavigationItemText-UserProfileSettings.Id.cshtml @@ -0,0 +1 @@ +@T["User Profile Settings"] diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/User.DetailAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/User.DetailAdmin.cshtml new file mode 100644 index 00000000000..52ca4c01b81 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/User.DetailAdmin.cshtml @@ -0,0 +1,23 @@ +@using OrchardCore.Mvc.Utilities; +@using OrchardCore.ContentManagement + +

@RenderTitleSegments(T["View User"])

+ +
+
+ @await DisplayAsync(Model.Header) + @if (Model.Meta != null) + { + + } +
+ @await DisplayAsync(Model.Content) + @if (Model.Footer != null) + { +
+ @await DisplayAsync(Model.Footer) +
+ } +
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/User.SummaryAdmin.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/User.SummaryAdmin.cshtml index 021b8dd1cce..79d58feb283 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/User.SummaryAdmin.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/User.SummaryAdmin.cshtml @@ -1,25 +1,34 @@ -
- @if (Model.Actions != null) - { -
- @await DisplayAsync(Model.Actions) +
+
+
+ @if (Model.Header != null) + { +
@await DisplayAsync(Model.Header)
+ } + + @if (Model.Meta != null) + { + + } + + @if (Model.Content != null) + { +
@await DisplayAsync(Model.Content)
+ }
- } - @if (Model.Header != null) - { -
@await DisplayAsync(Model.Header)
- } + @if (Model.Description != null) + { +
@await DisplayAsync(Model.Description)
+ } +
- @if (Model.Meta != null) + @if (Model.Actions != null) { - - -@if (Model.Content != null) -{ -
@await DisplayAsync(Model.Content)
-} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserButtons.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserButtons.cshtml index 8ec88436144..5401fff36df 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserButtons.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserButtons.cshtml @@ -6,30 +6,39 @@ @inject UserManager UserManager @inject IAuthorizationService AuthorizationService @{ - var user = Model.User as User; - var currentUser = user.UserName == User.Identity.Name; - var hasManageUser = await AuthorizationService.AuthorizeAsync(User, Permissions.ManageUsers, user); - var isLockedOut = await UserManager.IsLockedOutAsync(user); + var isCurrentUser = Model.User.UserName == User.Identity.Name; + var canEdit = await AuthorizationService.AuthorizeAsync(User, CommonPermissions.EditUsers, Model.User); + var isLockedOut = await UserManager.IsLockedOutAsync(Model.User); } -@T["Edit"] -@if (hasManageUser) +@if (canEdit) { - if (!currentUser) - { - @T["Delete"] - } - @T["Edit Password"] + @T["Edit"] +} + +@if (!isCurrentUser && await AuthorizationService.AuthorizeAsync(User, CommonPermissions.ViewUsers, Model.User)) +{ + @T["View"] +} + +@if (canEdit) +{ + @T["Edit Password"] if (isLockedOut) { - @T["Unlock"] + @T["Unlock"] } } -@if (!user.EmailConfirmed && Site.As().UsersMustValidateEmail && hasManageUser) +@if (!isCurrentUser && await AuthorizationService.AuthorizeAsync(User, CommonPermissions.DeleteUsers, Model.User)) +{ + @T["Delete"] +} + +@if (canEdit && !Model.User.EmailConfirmed && Site.As().UsersMustValidateEmail) {
- +
} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserEmail.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserEmail.Edit.cshtml new file mode 100644 index 00000000000..f8177c2a12e --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserEmail.Edit.cshtml @@ -0,0 +1,10 @@ +@model OrchardCore.Users.ViewModels.EditUserEmailViewModel + +
+ +
+ + + @T["The email is invalid."] +
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserInfo.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserInfo.cshtml new file mode 100644 index 00000000000..3f9adae94aa --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserInfo.cshtml @@ -0,0 +1,15 @@ +@model SummaryAdminUserViewModel + +

@T["User Info"]

+ +
+
@T["Username"]
+
@Model.User.UserName
+ +
@T["Email"]
+
@Model.User.Email
+ +
@T["Status"]
+
@(Model.User.IsEnabled ? T["Enabled"] : T["Disabled"])
+ +
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml index 1eda4f04865..38557e41adb 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/UserMenu.cshtml @@ -1,4 +1,8 @@ +@using System.Security.Claims @inject IAuthorizationService AuthorizationService +@{ + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); +}