diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Events/UserUpdatedEvent.cs b/dotnet-authserver/src/TeacherIdentity.AuthServer/Events/UserUpdatedEvent.cs index 568ff4445..15577514c 100644 --- a/dotnet-authserver/src/TeacherIdentity.AuthServer/Events/UserUpdatedEvent.cs +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Events/UserUpdatedEvent.cs @@ -24,6 +24,7 @@ public enum UserUpdatedEventChanges PreferredName = 1 << 8, TrnVerificationLevel = 1 << 9, NationalInsuranceNumber = 1 << 10, + TrnAssociationSource = 1 << 11, } public enum UserUpdatedEventSource diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml new file mode 100644 index 000000000..89496fd06 --- /dev/null +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml @@ -0,0 +1,35 @@ +@page "/admin/users/{userId}/elevate" +@model TeacherIdentity.AuthServer.Pages.Admin.ElevateUserTrnVerificationModel +@{ + ViewBag.Title = "Confirm TRN verification level elevation"; +} + +@section BeforeContent { + +} + +
+
+
+

@ViewBag.Title

+ + + + Email address + @Model.User!.EmailAddress + + + Name + @Model.User!.FirstName @Model.User!.LastName + + + TRN + @(Model.User!.Trn ?? "None") + + + + Continue +
+
+
+ diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml.cs b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml.cs new file mode 100644 index 000000000..26376c0e2 --- /dev/null +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/ElevateUserTrnVerification.cshtml.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.EntityFrameworkCore; +using TeacherIdentity.AuthServer.Infrastructure.Security; +using TeacherIdentity.AuthServer.Models; + +namespace TeacherIdentity.AuthServer.Pages.Admin; + +[Authorize(AuthorizationPolicies.GetAnIdentityAdmin)] +public class ElevateUserTrnVerificationModel : PageModel +{ + private readonly TeacherIdentityServerDbContext _dbContext; + private readonly IClock _clock; + public new User? User { get; set; } + [FromRoute] + public Guid UserId { get; set; } + + public ElevateUserTrnVerificationModel(TeacherIdentityServerDbContext dbContext, IClock clock) + { + _dbContext = dbContext; + _clock = clock; + } + + public void OnGet() + { + } + + public async Task OnPost() + { + if (!ModelState.IsValid) + { + return this.PageWithErrors(); + } + + User!.TrnVerificationLevel = null; + User!.TrnAssociationSource = TrnAssociationSource.SupportUi; + _dbContext.AddEvent(new Events.UserUpdatedEvent + { + Source = Events.UserUpdatedEventSource.SupportUi, + CreatedUtc = _clock.UtcNow, + Changes = Events.UserUpdatedEventChanges.TrnVerificationLevel | Events.UserUpdatedEventChanges.TrnAssociationSource, + User = User, + UpdatedByUserId = HttpContext.User.GetUserId(), + UpdatedByClientId = null + }); + await _dbContext.SaveChangesAsync(); + + TempData.SetFlashSuccess("TRN verification level elevated"); + + return RedirectToPage("/Admin/User", new { UserId }); + } + + public override async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next) + { + User = await GetUser(UserId); + + if (User == null) + { + context.Result = NotFound(); + return; + } + + if (User.EffectiveVerificationLevel != TrnVerificationLevel.Low) + { + context.Result = BadRequest(); + return; + } + + await next(); + } + + private async Task GetUser(Guid userId) + { + var user = await _dbContext.Users.SingleOrDefaultAsync(u => u.UserId == userId); + + return (user is null || user.UserType != UserType.Default) ? null : user; + } +} diff --git a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml index d7ed1fb78..5852a1306 100644 --- a/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml +++ b/dotnet-authserver/src/TeacherIdentity.AuthServer/Pages/Admin/User.cshtml @@ -131,6 +131,9 @@ TRN verification level Low + + Elevate + } else if(Model.EffectiveVerificationLevel == TrnVerificationLevel.Medium) diff --git a/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/ElevateUserTrnVerificationTests.cs b/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/ElevateUserTrnVerificationTests.cs new file mode 100644 index 000000000..6c32523bf --- /dev/null +++ b/dotnet-authserver/tests/TeacherIdentity.AuthServer.Tests/EndpointTests/Admin/ElevateUserTrnVerificationTests.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using TeacherIdentity.AuthServer.Events; +using TeacherIdentity.AuthServer.Models; + +namespace TeacherIdentity.AuthServer.Tests.EndpointTests.Admin; + +public class ElevateUserTrnVerificationTests : TestBase +{ + public ElevateUserTrnVerificationTests(HostFixture hostFixture) + : base(hostFixture) + { + } + + [Fact] + public async Task Get_UnauthenticatedUser_RedirectsToSignIn() + { + var user = await TestData.CreateUser(userType: UserType.Default, hasTrn: false); + + await UnauthenticatedUser_RedirectsToSignIn(HttpMethod.Get, $"/admin/users/{user.UserId}/elevate"); + } + + [Fact] + public async Task Get_AuthenticatedUserDoesNotHavePermission_ReturnsForbidden() + { + var user = await TestData.CreateUser(userType: UserType.Default, hasTrn: false); + + await AuthenticatedUserDoesNotHavePermission_ReturnsForbidden(HttpMethod.Get, $"/admin/users/{user.UserId}/elevate"); + } + + [Fact] + public async Task Get_UserDoesNotExist_ReturnsNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + var _ = await TestData.CreateUser(userType: UserType.Default, hasTrn: true); + var request = new HttpRequestMessage(HttpMethod.Get, $"/admin/users/{userId}/elevate"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_UserDoesNotExist_ReturnsNotFound() + { + // Arrange + var userId = Guid.NewGuid(); + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{userId}/elevate"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status404NotFound, (int)response.StatusCode); + } + + [Fact] + public async Task Post_UserTrnVerificationLevelNotLow_ReturnsBadRequest() + { + // Arrange + var user = await TestData.CreateUser(userType: UserType.Default, hasTrn: true, trnVerificationLevel: TrnVerificationLevel.Medium); + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{user.UserId}/elevate"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status400BadRequest, (int)response.StatusCode); + } + + [Fact] + public async Task Post_UserWithLowTrnVerificationLevel_UpdatesVerificationLevelRedirectsToUsers() + { + // Arrange + var user = await TestData.CreateUser(userType: UserType.Default, hasTrn: true, trnVerificationLevel: TrnVerificationLevel.Low); + var request = new HttpRequestMessage(HttpMethod.Post, $"/admin/users/{user.UserId}/elevate"); + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + Assert.Equal(StatusCodes.Status302Found, (int)response.StatusCode); + Assert.StartsWith($"/admin/users", response.Headers.Location?.OriginalString); + await TestData.WithDbContext(async dbContext => + { + var fetchedUser = await dbContext.Users.IgnoreQueryFilters().Where(u => u.UserId == user.UserId).SingleOrDefaultAsync(); + Assert.NotNull(fetchedUser); + Assert.Null(fetchedUser.TrnVerificationLevel); + Assert.Equal(TrnVerificationLevel.Medium, fetchedUser.EffectiveVerificationLevel); + Assert.Equal(TrnAssociationSource.SupportUi, fetchedUser.TrnAssociationSource); + }); + + EventObserver.AssertEventsSaved( + e => + { + var userChangedEvent = Assert.IsType(e); + Assert.Equal(Events.UserUpdatedEventChanges.TrnVerificationLevel | UserUpdatedEventChanges.TrnAssociationSource, userChangedEvent.Changes); + Assert.Equal(user.UserId, userChangedEvent.User.UserId); + Assert.Equal(Clock.UtcNow, userChangedEvent.CreatedUtc); + }); + } +}