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 {
+
+}
+
+
+
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);
+ });
+ }
+}