Admins can approve registered users (#5647)

* Users list: Cleanups

* Policies: Flip registration settings

* Policies: Add RequireUserApproval setting

* Add approval to user

* Require approval on login and for API key

* API handling

* AccountController cleanups

* Test fix

* Apply suggestions from code review

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>

* Add missing imports

* Communicate login requirements to user on account creation

* Add login requirements to basic auth handler

* Cleanups and test fix

* Encapsulate approval logic in user service and log approval changes

* Send follow up "Account approved" email

Closes #5656.

* Add notification for admins

* Fix creating a user via the admin view

* Update list: Unify flags into status column, add approve action

* Adjust "Resend email" wording

* Incorporate feedback from code review

* Remove duplicate test server policy reset

---------

Co-authored-by: Nicolas Dorier <nicolas.dorier@gmail.com>
This commit is contained in:
d11n
2024-01-31 06:45:54 +01:00
committed by GitHub
parent 411e0334d0
commit 6290b0f3bf
40 changed files with 1010 additions and 353 deletions

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
@@ -8,25 +7,21 @@ using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Data;
using BTCPayServer.Events;
using BTCPayServer.Models;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.EntityFrameworkCore;
using MimeKit;
namespace BTCPayServer.Controllers
{
public partial class UIServerController
{
[Route("server/users")]
[HttpGet("server/users")]
public async Task<IActionResult> ListUsers(
[FromServices] RoleManager<IdentityRole> roleManager,
UsersViewModel model,
string sortOrder = null
)
UsersViewModel model,
string sortOrder = null)
{
model = this.ParseListQuery(model ?? new UsersViewModel());
@@ -64,7 +59,8 @@ namespace BTCPayServer.Controllers
Name = u.UserName,
Email = u.Email,
Id = u.Id,
Verified = u.EmailConfirmed || !u.RequiresEmailConfirmation,
EmailConfirmed = u.RequiresEmailConfirmation ? u.EmailConfirmed : null,
Approved = u.RequiresApproval ? u.Approved : null,
Created = u.Created,
Roles = u.UserRoles.Select(role => role.RoleId),
Disabled = u.LockoutEnabled && u.LockoutEnd != null && DateTimeOffset.UtcNow < u.LockoutEnd.Value.UtcDateTime
@@ -74,44 +70,67 @@ namespace BTCPayServer.Controllers
return View(model);
}
[Route("server/users/{userId}")]
[HttpGet("server/users/{userId}")]
public new async Task<IActionResult> User(string userId)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
var roles = await _UserManager.GetRolesAsync(user);
var userVM = new UsersViewModel.UserViewModel
var model = new UsersViewModel.UserViewModel
{
Id = user.Id,
Email = user.Email,
Verified = user.EmailConfirmed || !user.RequiresEmailConfirmation,
EmailConfirmed = user.RequiresEmailConfirmation ? user.EmailConfirmed : null,
Approved = user.RequiresApproval ? user.Approved : null,
IsAdmin = Roles.HasServerAdmin(roles)
};
return View(userVM);
return View(model);
}
[Route("server/users/{userId}")]
[HttpPost]
[HttpPost("server/users/{userId}")]
public new async Task<IActionResult> User(string userId, UsersViewModel.UserViewModel viewModel)
{
var user = await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
bool? propertiesChanged = null;
bool? adminStatusChanged = null;
bool? approvalStatusChanged = null;
if (user.RequiresApproval && viewModel.Approved.HasValue)
{
approvalStatusChanged = await _userService.SetUserApproval(user.Id, viewModel.Approved.Value, Request.GetAbsoluteRootUri());
}
if (user.RequiresEmailConfirmation && viewModel.EmailConfirmed.HasValue && user.EmailConfirmed != viewModel.EmailConfirmed)
{
user.EmailConfirmed = viewModel.EmailConfirmed.Value;
propertiesChanged = true;
}
var admins = await _UserManager.GetUsersInRoleAsync(Roles.ServerAdmin);
var roles = await _UserManager.GetRolesAsync(user);
var wasAdmin = Roles.HasServerAdmin(roles);
if (!viewModel.IsAdmin && admins.Count == 1 && wasAdmin)
{
TempData[WellKnownTempData.ErrorMessage] = "This is the only Admin, so their role can't be removed until another Admin is added.";
return View(viewModel); // return
return View(viewModel);
}
if (viewModel.IsAdmin != wasAdmin)
{
var success = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
if (success)
adminStatusChanged = await _userService.SetAdminUser(user.Id, viewModel.IsAdmin);
}
if (propertiesChanged is true)
{
propertiesChanged = await _UserManager.UpdateAsync(user) is { Succeeded: true };
}
if (propertiesChanged.HasValue || adminStatusChanged.HasValue || approvalStatusChanged.HasValue)
{
if (propertiesChanged is not false && adminStatusChanged is not false && approvalStatusChanged is not false)
{
TempData[WellKnownTempData.SuccessMessage] = "User successfully updated";
}
@@ -121,23 +140,22 @@ namespace BTCPayServer.Controllers
}
}
return RedirectToAction(nameof(User), new { userId = userId });
return RedirectToAction(nameof(User), new { userId });
}
[Route("server/users/new")]
[HttpGet]
[HttpGet("server/users/new")]
public IActionResult CreateUser()
{
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
return View();
}
[Route("server/users/new")]
[HttpPost]
[HttpPost("server/users/new")]
public async Task<IActionResult> CreateUser(RegisterFromAdminViewModel model)
{
var requiresConfirmedEmail = _policiesSettings.RequiresConfirmedEmail;
ViewData["AllowRequestEmailConfirmation"] = requiresConfirmedEmail;
ViewData["AllowRequestApproval"] = _policiesSettings.RequiresUserApproval;
ViewData["AllowRequestEmailConfirmation"] = _policiesSettings.RequiresConfirmedEmail;
if (!_Options.CheatMode)
model.IsAdmin = false;
if (ModelState.IsValid)
@@ -148,7 +166,9 @@ namespace BTCPayServer.Controllers
UserName = model.Email,
Email = model.Email,
EmailConfirmed = model.EmailConfirmed,
RequiresEmailConfirmation = requiresConfirmedEmail,
RequiresEmailConfirmation = _policiesSettings.RequiresConfirmedEmail,
RequiresApproval = _policiesSettings.RequiresUserApproval,
Approved = model.Approved,
Created = DateTimeOffset.UtcNow
};
@@ -223,7 +243,6 @@ namespace BTCPayServer.Controllers
{
if (await _userService.IsUserTheOnlyOneAdmin(user))
{
// return
return View("Confirm", new ConfirmModel("Delete admin",
$"Unable to proceed: As the user <strong>{Html.Encode(user.Email)}</strong> is the last enabled admin, it cannot be removed."));
}
@@ -281,6 +300,29 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(ListUsers));
}
[HttpGet("server/users/{userId}/approve")]
public async Task<IActionResult> ApproveUser(string userId, bool approved)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
return View("Confirm", new ConfirmModel($"{(approved ? "Approve" : "Unapprove")} user", $"The user <strong>{Html.Encode(user.Email)}</strong> will be {(approved ? "approved" : "unapproved")}. Are you sure?", (approved ? "Approve" : "Unapprove")));
}
[HttpPost("server/users/{userId}/approve")]
public async Task<IActionResult> ApproveUserPost(string userId, bool approved)
{
var user = userId == null ? null : await _UserManager.FindByIdAsync(userId);
if (user == null)
return NotFound();
await _userService.SetUserApproval(userId, approved, Request.GetAbsoluteRootUri());
TempData[WellKnownTempData.SuccessMessage] = $"User {(approved ? "approved" : "unapproved")}";
return RedirectToAction(nameof(ListUsers));
}
[HttpGet("server/users/{userId}/verification-email")]
public async Task<IActionResult> SendVerificationEmail(string userId)
{
@@ -332,5 +374,8 @@ namespace BTCPayServer.Controllers
[Display(Name = "Email confirmed?")]
public bool EmailConfirmed { get; set; }
[Display(Name = "User approved?")]
public bool Approved { get; set; }
}
}