From b8bb27fc1446bcb02711845cb79b51de6a4ac874 Mon Sep 17 00:00:00 2001 From: FrostyApeOne Date: Thu, 12 Jun 2025 17:49:23 +0100 Subject: [PATCH 01/20] WIP - Added multi scheme authentication and up[dated the permission tables --- ...DfE.ExternalApplications.Api.Client.csproj | 7 ++ .../Extensions/ServiceCollectionExtensions.cs | 7 +- .../Generated/Client.g.cs | 90 ++++++++++++++++++ .../Generated/Contracts.g.cs | 15 +++ .../Generated/swagger.json | 27 ++++++ .../Security/BearerTokenHandler.cs | 16 +++- .../Controllers/TemplatesController.cs | 3 +- .../Controllers/UsersController.cs | 28 +++++- .../AuthenticationHeaderOperationFilter.cs | 22 +++-- .../Swagger/SwaggerOptions.cs | 25 ++++- .../appsettings.Development.json | 4 + .../Common/ResourceType.cs | 11 +++ .../Entities/Permission.cs | 3 + .../Entities/TemplatePermission.cs | 42 ++++++++ .../Entities/UserTemplateAccess.cs | 35 ------- .../ValueObjects/StronglyTypedIds.cs | 2 +- .../Database/ExternalApplicationsContext.cs | 95 +++++++++++-------- ...ExternalApplications.Infrastructure.csproj | 2 +- .../Security/AuthConstants.cs | 18 ++++ .../Authorization/AuthorizationExtensions.cs | 59 +++++++++++- .../Configurations/ExternalIdpOptions.cs | 8 ++ .../Aggregates/TemplatePermissionTests.cs | 74 +++++++++++++++ .../Aggregates/UserTemplateAccessTests.cs | 66 ------------- .../Entities/PermissionCustomization.cs | 3 + .../TemplatePermissionCustomization.cs | 38 ++++++++ .../Seeders/EaContextSeeder.cs | 33 +++++-- 26 files changed, 559 insertions(+), 174 deletions(-) create mode 100644 src/DfE.ExternalApplications.Domain/Common/ResourceType.cs create mode 100644 src/DfE.ExternalApplications.Domain/Entities/TemplatePermission.cs delete mode 100644 src/DfE.ExternalApplications.Domain/Entities/UserTemplateAccess.cs create mode 100644 src/DfE.ExternalApplications.Infrastructure/Security/AuthConstants.cs create mode 100644 src/DfE.ExternalApplications.Infrastructure/Security/Configurations/ExternalIdpOptions.cs create mode 100644 src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/TemplatePermissionTests.cs delete mode 100644 src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/UserTemplateAccessTests.cs create mode 100644 src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/TemplatePermissionCustomization.cs diff --git a/src/DfE.ExternalApplications.Api.Client/DfE.ExternalApplications.Api.Client.csproj b/src/DfE.ExternalApplications.Api.Client/DfE.ExternalApplications.Api.Client.csproj index 3023621b..ba36aaf6 100644 --- a/src/DfE.ExternalApplications.Api.Client/DfE.ExternalApplications.Api.Client.csproj +++ b/src/DfE.ExternalApplications.Api.Client/DfE.ExternalApplications.Api.Client.csproj @@ -23,6 +23,7 @@ + @@ -32,4 +33,10 @@ + + + ..\..\..\..\..\..\..\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\8.0.7\ref\net8.0\Microsoft.AspNetCore.Authentication.Abstractions.dll + + + diff --git a/src/DfE.ExternalApplications.Api.Client/Extensions/ServiceCollectionExtensions.cs b/src/DfE.ExternalApplications.Api.Client/Extensions/ServiceCollectionExtensions.cs index c79a7b94..90fc5c1c 100644 --- a/src/DfE.ExternalApplications.Api.Client/Extensions/ServiceCollectionExtensions.cs +++ b/src/DfE.ExternalApplications.Api.Client/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using DfE.ExternalApplications.Api.Client.Security; using DfE.ExternalApplications.Api.Client.Settings; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -43,10 +44,12 @@ public static IServiceCollection AddApiClient { var tokenService = serviceProvider.GetRequiredService(); - return new BearerTokenHandler(tokenService); + var httpContextAccessor = serviceProvider.GetRequiredService(); + + return new BearerTokenHandler(tokenService, httpContextAccessor); }); } return services; } } -} +} \ No newline at end of file diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs b/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs index 3e58ff49..60826708 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs +++ b/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs @@ -336,6 +336,96 @@ public string BaseUrl partial void PrepareRequest(System.Net.Http.HttpClient client, System.Net.Http.HttpRequestMessage request, System.Text.StringBuilder urlBuilder); partial void ProcessResponse(System.Net.Http.HttpClient client, System.Net.Http.HttpResponseMessage response); + /// + /// Returns all my permissions. + /// + /// A UserPermission object representing the User's Permissions. + /// A server side error occurred. + public virtual System.Threading.Tasks.Task> GetMyPermissionsAsync() + { + return GetMyPermissionsAsync(System.Threading.CancellationToken.None); + } + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Returns all my permissions. + /// + /// A UserPermission object representing the User's Permissions. + /// A server side error occurred. + public virtual async System.Threading.Tasks.Task> GetMyPermissionsAsync(System.Threading.CancellationToken cancellationToken) + { + var client_ = _httpClient; + var disposeClient_ = false; + try + { + using (var request_ = new System.Net.Http.HttpRequestMessage()) + { + request_.Method = new System.Net.Http.HttpMethod("GET"); + request_.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("application/json")); + + var urlBuilder_ = new System.Text.StringBuilder(); + if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); + // Operation Path: "v1/me/permissions" + urlBuilder_.Append("v1/me/permissions"); + + PrepareRequest(client_, request_, urlBuilder_); + + var url_ = urlBuilder_.ToString(); + request_.RequestUri = new System.Uri(url_, System.UriKind.RelativeOrAbsolute); + + PrepareRequest(client_, request_, url_); + + var response_ = await client_.SendAsync(request_, System.Net.Http.HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); + var disposeResponse_ = true; + try + { + var headers_ = new System.Collections.Generic.Dictionary>(); + foreach (var item_ in response_.Headers) + headers_[item_.Key] = item_.Value; + if (response_.Content != null && response_.Content.Headers != null) + { + foreach (var item_ in response_.Content.Headers) + headers_[item_.Key] = item_.Value; + } + + ProcessResponse(client_, response_); + + var status_ = (int)response_.StatusCode; + if (status_ == 200) + { + var objectResponse_ = await ReadObjectResponseAsync>(response_, headers_, cancellationToken).ConfigureAwait(false); + if (objectResponse_.Object == null) + { + throw new PersonsApiException("Response was null which was not expected.", status_, objectResponse_.Text, headers_, null); + } + return objectResponse_.Object; + } + else + if (status_ == 401) + { + string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new PersonsApiException("Unauthorized \u2013 no valid user token", status_, responseText_, headers_, null); + } + else + { + var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new PersonsApiException("The HTTP status code of the response was not expected (" + status_ + ").", status_, responseData_, headers_, null); + } + } + finally + { + if (disposeResponse_) + response_.Dispose(); + } + } + } + finally + { + if (disposeClient_) + client_.Dispose(); + } + } + /// /// Returns all permissions for the user by {email}. /// diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs b/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs index 1c48c29c..7a0f24c8 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs +++ b/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs @@ -48,6 +48,21 @@ public partial interface ITemplatesClient [System.CodeDom.Compiler.GeneratedCode("NSwag", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial interface IUsersClient { + /// + /// Returns all my permissions. + /// + /// A UserPermission object representing the User's Permissions. + /// A server side error occurred. + System.Threading.Tasks.Task> GetMyPermissionsAsync(); + + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// + /// Returns all my permissions. + /// + /// A UserPermission object representing the User's Permissions. + /// A server side error occurred. + System.Threading.Tasks.Task> GetMyPermissionsAsync(System.Threading.CancellationToken cancellationToken); + /// /// Returns all permissions for the user by {email}. /// diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json b/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json index d5734527..c4a3c987 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json +++ b/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json @@ -51,6 +51,33 @@ } } }, + "/v1/me/permissions": { + "get": { + "tags": [ + "Users" + ], + "summary": "Returns all my permissions.", + "operationId": "Users_GetMyPermissions", + "responses": { + "200": { + "description": "A UserPermission object representing the User's Permissions.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserPermissionDto" + } + } + } + } + }, + "401": { + "description": "Unauthorized – no valid user token" + } + } + } + }, "/v1/Users/{email}/permissions": { "get": { "tags": [ diff --git a/src/DfE.ExternalApplications.Api.Client/Security/BearerTokenHandler.cs b/src/DfE.ExternalApplications.Api.Client/Security/BearerTokenHandler.cs index cfe43b9b..3d76444f 100644 --- a/src/DfE.ExternalApplications.Api.Client/Security/BearerTokenHandler.cs +++ b/src/DfE.ExternalApplications.Api.Client/Security/BearerTokenHandler.cs @@ -1,16 +1,28 @@ using System.Diagnostics.CodeAnalysis; using System.Net.Http.Headers; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; namespace DfE.ExternalApplications.Api.Client.Security { [ExcludeFromCodeCoverage] - public class BearerTokenHandler(ITokenAcquisitionService tokenAcquisitionService) : DelegatingHandler + public class BearerTokenHandler(ITokenAcquisitionService tokenAcquisitionService, IHttpContextAccessor httpCtx) : DelegatingHandler { protected override async Task SendAsync(HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { var token = await tokenAcquisitionService.GetTokenAsync(); - request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + // Service-To-Service token + request.Headers.Add("X-Service-Authorization", token); + + // User Token ExtIdP + var userToken = await httpCtx.HttpContext! + .GetTokenAsync("id_token"); + if (!string.IsNullOrEmpty(userToken)) + { + request.Headers.Authorization = + new AuthenticationHeaderValue("Bearer", userToken); + } return await base.SendAsync(request, cancellationToken); } diff --git a/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs b/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs index f59362eb..4548e144 100644 --- a/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs +++ b/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs @@ -19,7 +19,8 @@ public class TemplatesController(ISender sender) : ControllerBase [HttpGet("{templateName}/schema/{userId}")] [SwaggerResponse(200, "The latest template schema.", typeof(TemplateSchemaDto))] [SwaggerResponse(400, "Request was invalid or access denied.")] - [AllowAnonymous] + [Authorize(Policy = "CanRead")] + [Authorize(Policy = "CanRead")] public async Task GetLatestTemplateSchemaAsync( [FromRoute] string templateName, [FromRoute] Guid userId, diff --git a/src/DfE.ExternalApplications.Api/Controllers/UsersController.cs b/src/DfE.ExternalApplications.Api/Controllers/UsersController.cs index 21db282f..bf6fd731 100644 --- a/src/DfE.ExternalApplications.Api/Controllers/UsersController.cs +++ b/src/DfE.ExternalApplications.Api/Controllers/UsersController.cs @@ -1,6 +1,8 @@ +using System.Security.Claims; using Asp.Versioning; using DfE.CoreLibs.Contracts.ExternalApplications.Models.Response; using DfE.ExternalApplications.Application.Users.Queries; +using DfE.ExternalApplications.Infrastructure.Security; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -13,13 +15,35 @@ namespace DfE.ExternalApplications.Api.Controllers; [Route("v{version:apiVersion}/[controller]")] public class UsersController(ISender sender) : ControllerBase { + /// + /// Returns all my permissions. + /// + [HttpGet("/v{version:apiVersion}/me/permissions")] + [Authorize(AuthenticationSchemes = AuthConstants.UserScheme)] + [SwaggerResponse(200, "A UserPermission object representing the User's Permissions.", typeof(IReadOnlyCollection))] + [SwaggerResponse(401, "Unauthorized – no valid user token")] + public async Task GetMyPermissionsAsync( + CancellationToken cancellationToken) + { + var email = User.FindFirstValue(ClaimTypes.Email) + ?? throw new InvalidOperationException("No email claim in token"); + + var query = new GetAllUserPermissionsQuery(email); + var result = await sender.Send(query, cancellationToken); + + if (!result.IsSuccess) + return BadRequest(result.Error); + + return Ok(result.Value); + } + /// /// Returns all permissions for the user by {email}. /// [HttpGet("{email}/permissions")] [SwaggerResponse(200, "A UserPermission object representing the User's Permissions.", typeof(IReadOnlyCollection))] [SwaggerResponse(400, "Email cannot be null or empty.")] - [AllowAnonymous] + [Authorize] public async Task GetAllPermissionsForUserAsync( [FromRoute] string email, CancellationToken cancellationToken) @@ -32,4 +56,6 @@ public async Task GetAllPermissionsForUserAsync( return Ok(result.Value); } + + } diff --git a/src/DfE.ExternalApplications.Api/Swagger/AuthenticationHeaderOperationFilter.cs b/src/DfE.ExternalApplications.Api/Swagger/AuthenticationHeaderOperationFilter.cs index ef0a0e7a..ae9421a5 100644 --- a/src/DfE.ExternalApplications.Api/Swagger/AuthenticationHeaderOperationFilter.cs +++ b/src/DfE.ExternalApplications.Api/Swagger/AuthenticationHeaderOperationFilter.cs @@ -9,14 +9,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) { operation.Security ??= new List(); - var securityScheme = new OpenApiSecurityScheme + var userScheme = new OpenApiSecurityScheme { - Scheme = "Bearer", - BearerFormat = "JWT", - In = ParameterLocation.Header, - Name = "Authorization", - Type = SecuritySchemeType.Http, - Description = "Input your Bearer token in this format - Bearer {your token here}", Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, @@ -24,11 +18,21 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context) } }; - operation.Security.Add(new OpenApiSecurityRequirement + var svcScheme = new OpenApiSecurityScheme { + Reference = new OpenApiReference { - securityScheme, new List() + Type = ReferenceType.SecurityScheme, + Id = "ServiceBearer" } + }; + operation.Security.Add(new OpenApiSecurityRequirement + { + [userScheme] = Array.Empty() + }); + operation.Security.Add(new OpenApiSecurityRequirement + { + [svcScheme] = Array.Empty() }); } } diff --git a/src/DfE.ExternalApplications.Api/Swagger/SwaggerOptions.cs b/src/DfE.ExternalApplications.Api/Swagger/SwaggerOptions.cs index 776a07b6..db3cc9d9 100644 --- a/src/DfE.ExternalApplications.Api/Swagger/SwaggerOptions.cs +++ b/src/DfE.ExternalApplications.Api/Swagger/SwaggerOptions.cs @@ -39,11 +39,32 @@ public void Configure(SwaggerGenOptions options) options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { - Description = "JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", + Description = "User JWT Authorization header using the Bearer scheme. Example: \"Authorization: Bearer {token}\"", Name = "Authorization", In = ParameterLocation.Header, Type = SecuritySchemeType.Http, - Scheme = "Bearer" + Scheme = "bearer", + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }); + + options.AddSecurityDefinition("ServiceBearer", new OpenApiSecurityScheme + { + Description = "Service-to-service bearer token. Example: X-Service-Authorization: {token}", + Name = "X-Service-Authorization", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ServiceBearer" + } }); options.OperationFilter(); diff --git a/src/DfE.ExternalApplications.Api/appsettings.Development.json b/src/DfE.ExternalApplications.Api/appsettings.Development.json index 44ee7395..ecb1b53b 100644 --- a/src/DfE.ExternalApplications.Api/appsettings.Development.json +++ b/src/DfE.ExternalApplications.Api/appsettings.Development.json @@ -25,5 +25,9 @@ }, "Features": { "PerformanceLoggingEnabled": true + }, + "DfESignIn": { + "Authority": "https://test-oidcccc.signin.education.gov.uk", + "ClientId": "RSDExternalAccccpps" } } \ No newline at end of file diff --git a/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs b/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs new file mode 100644 index 00000000..20ff9a9e --- /dev/null +++ b/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs @@ -0,0 +1,11 @@ +namespace DfE.ExternalApplications.Domain.Common +{ + public enum ResourceType + { + Application, + TaskGroup, + Task, + Page, + Field + } +} diff --git a/src/DfE.ExternalApplications.Domain/Entities/Permission.cs b/src/DfE.ExternalApplications.Domain/Entities/Permission.cs index 613a3fce..9b110d6d 100644 --- a/src/DfE.ExternalApplications.Domain/Entities/Permission.cs +++ b/src/DfE.ExternalApplications.Domain/Entities/Permission.cs @@ -12,6 +12,7 @@ public sealed class Permission : IEntity public User? User { get; private set; } public ApplicationId ApplicationId { get; private set; } public Application? Application { get; private set; } + public ResourceType ResourceType { get; private set; } public string ResourceKey { get; private set; } = null!; public AccessType AccessType { get; private set; } public DateTime GrantedOn { get; private set; } @@ -26,6 +27,7 @@ public Permission( UserId userId, ApplicationId applicationId, string resourceKey, + ResourceType resourceType, AccessType accessType, DateTime grantedOn, UserId grantedBy) @@ -34,6 +36,7 @@ public Permission( UserId = userId; ApplicationId = applicationId; ResourceKey = resourceKey ?? throw new ArgumentNullException(nameof(resourceKey)); + ResourceType = resourceType; AccessType = accessType; GrantedOn = grantedOn; GrantedBy = grantedBy; diff --git a/src/DfE.ExternalApplications.Domain/Entities/TemplatePermission.cs b/src/DfE.ExternalApplications.Domain/Entities/TemplatePermission.cs new file mode 100644 index 00000000..1e5179c1 --- /dev/null +++ b/src/DfE.ExternalApplications.Domain/Entities/TemplatePermission.cs @@ -0,0 +1,42 @@ +using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.ExternalApplications.Domain.Common; +using DfE.ExternalApplications.Domain.ValueObjects; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DfE.ExternalApplications.Domain.Entities +{ + public sealed class TemplatePermission : BaseAggregateRoot, IEntity + { + public TemplatePermissionId? Id { get; private set; } + public UserId UserId { get; private set; } + public User? User { get; private set; } + public TemplateId TemplateId { get; private set; } + public Template? Template { get; private set; } + public AccessType AccessType { get; private set; } + public DateTime GrantedOn { get; private set; } + public UserId GrantedBy { get; private set; } + public User? GrantedByUser { get; private set; } + + private TemplatePermission() { /* For EF Core */ } + + public TemplatePermission( + TemplatePermissionId id, + UserId userId, + TemplateId templateId, + AccessType accessType, + DateTime grantedOn, + UserId grantedBy) + { + Id = id ?? throw new ArgumentNullException(nameof(id)); + UserId = userId ?? throw new ArgumentNullException(nameof(userId)); + TemplateId = templateId ?? throw new ArgumentNullException(nameof(templateId)); + AccessType = accessType; + GrantedOn = grantedOn; + GrantedBy = grantedBy; + } + } +} diff --git a/src/DfE.ExternalApplications.Domain/Entities/UserTemplateAccess.cs b/src/DfE.ExternalApplications.Domain/Entities/UserTemplateAccess.cs deleted file mode 100644 index 5c25c7fe..00000000 --- a/src/DfE.ExternalApplications.Domain/Entities/UserTemplateAccess.cs +++ /dev/null @@ -1,35 +0,0 @@ -using DfE.ExternalApplications.Domain.Common; -using DfE.ExternalApplications.Domain.ValueObjects; - -namespace DfE.ExternalApplications.Domain.Entities; - -public sealed class UserTemplateAccess : BaseAggregateRoot, IEntity -{ - public UserTemplateAccessId? Id { get; private set; } - public UserId UserId { get; private set; } - public User? User { get; private set; } - public TemplateId TemplateId { get; private set; } - public Template? Template { get; private set; } - public DateTime GrantedOn { get; private set; } - public UserId GrantedBy { get; private set; } - public User? GrantedByUser { get; private set; } - - private UserTemplateAccess() { /* For EF Core */ } - - /// - /// Constructs a new UserTemplateAccess. - /// - public UserTemplateAccess( - UserTemplateAccessId id, - UserId userId, - TemplateId templateId, - DateTime grantedOn, - UserId grantedBy) - { - Id = id ?? throw new ArgumentNullException(nameof(id)); - UserId = userId ?? throw new ArgumentNullException(nameof(userId)); - TemplateId = templateId ?? throw new ArgumentNullException(nameof(templateId)); - GrantedOn = grantedOn; - GrantedBy = grantedBy; - } -} \ No newline at end of file diff --git a/src/DfE.ExternalApplications.Domain/ValueObjects/StronglyTypedIds.cs b/src/DfE.ExternalApplications.Domain/ValueObjects/StronglyTypedIds.cs index b376187b..a7f5a46c 100644 --- a/src/DfE.ExternalApplications.Domain/ValueObjects/StronglyTypedIds.cs +++ b/src/DfE.ExternalApplications.Domain/ValueObjects/StronglyTypedIds.cs @@ -9,6 +9,6 @@ public record TemplateVersionId(Guid Value) : IStronglyTypedId; public record ApplicationId(Guid Value) : IStronglyTypedId; public record ResponseId(Guid Value) : IStronglyTypedId; public record PermissionId(Guid Value) : IStronglyTypedId; - public record UserTemplateAccessId(Guid Value) : IStronglyTypedId; public record TaskAssignmentLabelId(Guid Value) : IStronglyTypedId; + public record TemplatePermissionId(Guid Value) : IStronglyTypedId; } diff --git a/src/DfE.ExternalApplications.Infrastructure/Database/ExternalApplicationsContext.cs b/src/DfE.ExternalApplications.Infrastructure/Database/ExternalApplicationsContext.cs index 7022406e..3c4bb774 100644 --- a/src/DfE.ExternalApplications.Infrastructure/Database/ExternalApplicationsContext.cs +++ b/src/DfE.ExternalApplications.Infrastructure/Database/ExternalApplicationsContext.cs @@ -1,4 +1,5 @@ using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.ExternalApplications.Domain.Common; using DfE.ExternalApplications.Domain.Entities; using DfE.ExternalApplications.Domain.ValueObjects; using DfE.ExternalApplications.Infrastructure.Database.Interceptors; @@ -35,8 +36,8 @@ public ExternalApplicationsContext(DbContextOptions public DbSet Applications { get; set; } = null!; public DbSet ApplicationResponses { get; set; } = null!; public DbSet Permissions { get; set; } = null!; - public DbSet UserTemplateAccesses { get; set; } = null!; public DbSet TaskAssignmentLabels { get; set; } = null!; + public DbSet TemplatePermissions { get; set; } = null!; protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { @@ -59,7 +60,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity(ConfigureApplication); modelBuilder.Entity(ConfigureApplicationResponse); modelBuilder.Entity(ConfigurePermission); - modelBuilder.Entity(ConfigureUserTemplateAccess); + modelBuilder.Entity(ConfigureTemplatePermission); modelBuilder.Entity(ConfigureTaskAssignmentLabel); base.OnModelCreating(modelBuilder); @@ -323,6 +324,12 @@ private static void ConfigurePermission(EntityTypeBuilder b) .HasColumnName("ApplicationId") .HasConversion(v => v.Value, v => new ApplicationId(v)) .IsRequired(); + b.Property(e => e.ResourceType) + .HasColumnName("ResourceType") + .HasConversion( + v => (byte)v, + v => (ResourceType)v) + .IsRequired(); b.Property(e => e.ResourceKey) .HasColumnName("ResourceKey") .HasMaxLength(200) @@ -354,44 +361,6 @@ private static void ConfigurePermission(EntityTypeBuilder b) .OnDelete(DeleteBehavior.Restrict); } - private static void ConfigureUserTemplateAccess(EntityTypeBuilder b) - { - b.ToTable("UserTemplateAccess", DefaultSchema); - b.HasKey(e => e.Id); - b.Property(e => e.Id) - .HasColumnName("UserTemplateAccessId") - .ValueGeneratedOnAdd() - .HasConversion(v => v.Value, v => new UserTemplateAccessId(v)) - .IsRequired(); - b.Property(e => e.UserId) - .HasColumnName("UserId") - .HasConversion(v => v.Value, v => new UserId(v)) - .IsRequired(); - b.Property(e => e.TemplateId) - .HasColumnName("TemplateId") - .HasConversion(v => v.Value, v => new TemplateId(v)) - .IsRequired(); - b.Property(e => e.GrantedOn) - .HasColumnName("GrantedOn") - .HasDefaultValueSql("GETDATE()") - .IsRequired(); - b.Property(e => e.GrantedBy) - .HasColumnName("GrantedBy") - .HasConversion(v => v.Value, v => new UserId(v)) - .IsRequired(); - - b.HasOne(e => e.User) - .WithMany() - .HasForeignKey(e => e.UserId); - b.HasOne(e => e.Template) - .WithMany() - .HasForeignKey(e => e.TemplateId); - b.HasOne(e => e.GrantedByUser) - .WithMany() - .HasForeignKey(e => e.GrantedBy) - .OnDelete(DeleteBehavior.Restrict); - } - private static void ConfigureTaskAssignmentLabel(EntityTypeBuilder b) { b.ToTable("TaskAssignmentLabels", DefaultSchema); @@ -431,4 +400,50 @@ private static void ConfigureTaskAssignmentLabel(EntityTypeBuilder e.CreatedBy) .OnDelete(DeleteBehavior.Restrict); } + + private static void ConfigureTemplatePermission(EntityTypeBuilder b) + { + b.ToTable("TemplatePermissions", DefaultSchema); + b.HasKey(e => e.Id); + b.Property(e => e.Id) + .HasColumnName("TemplatePermissionId") + .ValueGeneratedOnAdd() + .HasConversion(v => v.Value, v => new TemplatePermissionId(v)) + .IsRequired(); + b.Property(e => e.UserId) + .HasColumnName("UserId") + .HasConversion(v => v.Value, v => new UserId(v)) + .IsRequired(); + b.Property(e => e.TemplateId) + .HasColumnName("TemplateId") + .HasConversion(v => v.Value, v => new TemplateId(v)) + .IsRequired(); + b.Property(e => e.AccessType) + .HasColumnName("AccessType") + .HasConversion( + v => (byte)v, + v => (AccessType)v) + .IsRequired(); + b.Property(e => e.GrantedOn) + .HasColumnName("GrantedOn") + .HasDefaultValueSql("GETDATE()") + .IsRequired(); + b.Property(e => e.GrantedBy) + .HasColumnName("GrantedBy") + .HasConversion(v => v.Value, v => new UserId(v)) + .IsRequired(); + + b.HasOne(e => e.User) + .WithMany() + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.Cascade); + b.HasOne(e => e.Template) + .WithMany() + .HasForeignKey(e => e.TemplateId); + b.HasOne(e => e.GrantedByUser) + .WithMany() + .HasForeignKey(e => e.GrantedBy) + .OnDelete(DeleteBehavior.Restrict); + } + } diff --git a/src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj b/src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj index ac3faaff..8528e16d 100644 --- a/src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj +++ b/src/DfE.ExternalApplications.Infrastructure/DfE.ExternalApplications.Infrastructure.csproj @@ -5,7 +5,7 @@ - + diff --git a/src/DfE.ExternalApplications.Infrastructure/Security/AuthConstants.cs b/src/DfE.ExternalApplications.Infrastructure/Security/AuthConstants.cs new file mode 100644 index 00000000..931d6a8e --- /dev/null +++ b/src/DfE.ExternalApplications.Infrastructure/Security/AuthConstants.cs @@ -0,0 +1,18 @@ +namespace DfE.ExternalApplications.Infrastructure.Security +{ + public static class AuthConstants + { + // Scheme names + public const string UserScheme = "UserScheme"; + public const string AzureAdScheme = "AzureAdScheme"; + + // Header names & prefixes + public const string AuthorizationHeader = "Authorization"; + public const string ServiceAuthHeader = "X-Service-Authorization"; + public const string BearerPrefix = "Bearer "; + + // Configuration sections + public const string ExternalIdpSection = "DfESignIn"; + public const string AzureAdSection = "AzureAd"; + } +} diff --git a/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/AuthorizationExtensions.cs b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/AuthorizationExtensions.cs index 59b01131..358ed4b1 100644 --- a/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/AuthorizationExtensions.cs +++ b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/AuthorizationExtensions.cs @@ -1,12 +1,10 @@ using DfE.CoreLibs.Security.Authorization; -using DfE.ExternalApplications.Infrastructure.Security.Configurations; using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Identity.Web; using System.Diagnostics.CodeAnalysis; -using DfE.CoreLibs.Security.Authorization; +using DfE.ExternalApplications.Infrastructure.Security.Configurations; namespace DfE.ExternalApplications.Infrastructure.Security.Authorization { @@ -16,8 +14,59 @@ public static class AuthorizationExtensions public static IServiceCollection AddCustomAuthorization(this IServiceCollection services, IConfiguration configuration) { - services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) - .AddMicrosoftIdentityWebApi(configuration.GetSection("AzureAd")); + services.Configure( + configuration.GetSection(AuthConstants.ExternalIdpSection)); + + var externalOpts = configuration + .GetSection(AuthConstants.ExternalIdpSection) + .Get()!; + + var auth = services.AddAuthentication(); + + // User Scheme + auth.AddJwtBearer(AuthConstants.UserScheme, opts => + { + opts.Authority = externalOpts.Authority; + opts.Audience = externalOpts.ClientId; + + opts.Events = new JwtBearerEvents + { + OnMessageReceived = ctx => + { + var hdr = ctx.Request.Headers[AuthConstants.AuthorizationHeader] + .ToString(); + if (hdr.StartsWith(AuthConstants.BearerPrefix, + StringComparison.OrdinalIgnoreCase)) + { + ctx.Token = hdr[AuthConstants.BearerPrefix.Length..].Trim(); + } + return Task.CompletedTask; + } + }; + }); + + // Service-Scheme Azure AD + auth.AddMicrosoftIdentityWebApi( + configureMicrosoftIdentityOptions: opts => + { + configuration + .GetSection(AuthConstants.AzureAdSection) + .Bind(opts); + }, + configureJwtBearerOptions: opts => + { + opts.Events ??= new JwtBearerEvents(); + opts.Events.OnMessageReceived = ctx => + { + ctx.Token = ctx.Request + .Headers[AuthConstants.ServiceAuthHeader] + .FirstOrDefault(); + return Task.CompletedTask; + }; + }, + jwtBearerScheme: AuthConstants.AzureAdScheme, + subscribeToJwtBearerMiddlewareDiagnosticsEvents: false + ); services.AddApplicationAuthorization(configuration); diff --git a/src/DfE.ExternalApplications.Infrastructure/Security/Configurations/ExternalIdpOptions.cs b/src/DfE.ExternalApplications.Infrastructure/Security/Configurations/ExternalIdpOptions.cs new file mode 100644 index 00000000..b501cc27 --- /dev/null +++ b/src/DfE.ExternalApplications.Infrastructure/Security/Configurations/ExternalIdpOptions.cs @@ -0,0 +1,8 @@ +namespace DfE.ExternalApplications.Infrastructure.Security.Configurations +{ + public class ExternalIdpOptions + { + public string Authority { get; set; } = ""; + public string ClientId { get; set; } = ""; + } +} diff --git a/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/TemplatePermissionTests.cs b/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/TemplatePermissionTests.cs new file mode 100644 index 00000000..bd665482 --- /dev/null +++ b/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/TemplatePermissionTests.cs @@ -0,0 +1,74 @@ +using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.CoreLibs.Testing.AutoFixture.Attributes; +using DfE.ExternalApplications.Domain.Entities; +using DfE.ExternalApplications.Domain.ValueObjects; +using DfE.ExternalApplications.Tests.Common.Customizations.Entities; + +namespace DfE.ExternalApplications.Domain.Tests.Aggregates +{ + public class TemplatePermissionTests + { + [Theory] + [CustomAutoData(typeof(TemplatePermissionCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenIdIsNull( + UserId userId, + TemplateId templateId, + AccessType type, + DateTime grantedOn, + UserId grantedBy) + { + var ex = Assert.Throws(() => + new TemplatePermission( + null!, + userId, + templateId, + type, + grantedOn, + grantedBy)); + + Assert.Equal("id", ex.ParamName); + } + + [Theory] + [CustomAutoData(typeof(TemplatePermissionCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenUserIdIsNull( + TemplatePermissionId id, + TemplateId templateId, + AccessType type, + DateTime grantedOn, + UserId grantedBy) + { + var ex = Assert.Throws(() => + new TemplatePermission( + id, + null!, + templateId, + type, + grantedOn, + grantedBy)); + + Assert.Equal("userId", ex.ParamName); + } + + [Theory] + [CustomAutoData(typeof(TemplatePermissionCustomization))] + public void Constructor_ShouldThrowArgumentNullException_WhenTemplateIdIsNull( + TemplatePermissionId id, + UserId userId, + AccessType type, + DateTime grantedOn, + UserId grantedBy) + { + var ex = Assert.Throws(() => + new TemplatePermission( + id, + userId, + null!, + type, + grantedOn, + grantedBy)); + + Assert.Equal("templateId", ex.ParamName); + } + } +} diff --git a/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/UserTemplateAccessTests.cs b/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/UserTemplateAccessTests.cs deleted file mode 100644 index d32cb3bc..00000000 --- a/src/Tests/DfE.ExternalApplications.Domain.Tests/Aggregates/UserTemplateAccessTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -using DfE.CoreLibs.Testing.AutoFixture.Attributes; -using DfE.ExternalApplications.Domain.Entities; -using DfE.ExternalApplications.Domain.ValueObjects; -using DfE.ExternalApplications.Tests.Common.Customizations.Entities; - -namespace DfE.ExternalApplications.Domain.Tests.Aggregates; - -public class UserTemplateAccessTests -{ - [Theory] - [CustomAutoData(typeof(UserTemplateAccessCustomization))] - public void Constructor_ShouldThrowArgumentNullException_WhenIdIsNull( - UserId userId, - TemplateId templateId, - DateTime grantedOn, - UserId grantedBy) - { - var ex = Assert.Throws(() => - new UserTemplateAccess( - null!, // id - userId, - templateId, - grantedOn, - grantedBy)); - - Assert.Equal("id", ex.ParamName); - } - - [Theory] - [CustomAutoData(typeof(UserTemplateAccessCustomization))] - public void Constructor_ShouldThrowArgumentNullException_WhenUserIdIsNull( - UserTemplateAccessId id, - TemplateId templateId, - DateTime grantedOn, - UserId grantedBy) - { - var ex = Assert.Throws(() => - new UserTemplateAccess( - id, - null!, // userId - templateId, - grantedOn, - grantedBy)); - - Assert.Equal("userId", ex.ParamName); - } - - [Theory] - [CustomAutoData(typeof(UserTemplateAccessCustomization))] - public void Constructor_ShouldThrowArgumentNullException_WhenTemplateIdIsNull( - UserTemplateAccessId id, - UserId userId, - DateTime grantedOn, - UserId grantedBy) - { - var ex = Assert.Throws(() => - new UserTemplateAccess( - id, - userId, - null!, // templateId - grantedOn, - grantedBy)); - - Assert.Equal("templateId", ex.ParamName); - } -} \ No newline at end of file diff --git a/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/PermissionCustomization.cs b/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/PermissionCustomization.cs index 69e29465..e64ea4f2 100644 --- a/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/PermissionCustomization.cs +++ b/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/PermissionCustomization.cs @@ -1,6 +1,7 @@ using AutoFixture; using AutoFixture.Kernel; using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.ExternalApplications.Domain.Common; using DfE.ExternalApplications.Domain.Entities; using DfE.ExternalApplications.Domain.ValueObjects; using ApplicationId = DfE.ExternalApplications.Domain.ValueObjects.ApplicationId; @@ -15,6 +16,7 @@ public class PermissionCustomization : ICustomization public UserId? OverrideUserId { get; set; } public ApplicationId? OverrideAppId { get; set; } public string? OverrideResourceKey { get; set; } + public ResourceType? OverrideResourceType { get; set; } public AccessType? OverrideAccessType { get; set; } public DateTime? OverrideGrantedOn { get; set; } public UserId? OverrideGrantedBy { get; set; } @@ -33,6 +35,7 @@ public void Customize(IFixture fixture) var appId = OverrideAppId ?? new ApplicationId(fixture.Create()); var resourceKey = OverrideResourceKey ?? fixture.Create(); var accessType = OverrideAccessType ?? fixture.Create(); + var resourceType = OverrideResourceType ?? fixture.Create(); var grantedOn = OverrideGrantedOn ?? fixture.Create(); var grantedBy = OverrideGrantedBy ?? new UserId(fixture.Create()); diff --git a/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/TemplatePermissionCustomization.cs b/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/TemplatePermissionCustomization.cs new file mode 100644 index 00000000..83e80937 --- /dev/null +++ b/src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/TemplatePermissionCustomization.cs @@ -0,0 +1,38 @@ +using AutoFixture; +using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.ExternalApplications.Domain.Entities; +using DfE.ExternalApplications.Domain.ValueObjects; + +namespace DfE.ExternalApplications.Tests.Common.Customizations.Entities +{ + public class TemplatePermissionCustomization : ICustomization + { + public TemplatePermissionId? OverrideId { get; set; } + public UserId? OverrideUserId { get; set; } + public TemplateId? OverrideTemplateId { get; set; } + public AccessType? OverridePermissionType { get; set; } + public DateTime? OverrideGrantedOn { get; set; } + public UserId? OverrideGrantedBy { get; set; } + + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer.FromFactory(() => + { + var id = OverrideId ?? new TemplatePermissionId(fixture.Create()); + var userId = OverrideUserId ?? new UserId(fixture.Create()); + var templateId = OverrideTemplateId ?? new TemplateId(fixture.Create()); + var permissionType = OverridePermissionType ?? fixture.Create(); + var grantedOn = OverrideGrantedOn ?? fixture.Create(); + var grantedBy = OverrideGrantedBy ?? new UserId(fixture.Create()); + + return new TemplatePermission( + id, + userId, + templateId, + permissionType, + grantedOn, + grantedBy); + })); + } + } +} diff --git a/src/Tests/DfE.ExternalApplications.Tests.Common/Seeders/EaContextSeeder.cs b/src/Tests/DfE.ExternalApplications.Tests.Common/Seeders/EaContextSeeder.cs index 9e5fcfe0..c5a7a3de 100644 --- a/src/Tests/DfE.ExternalApplications.Tests.Common/Seeders/EaContextSeeder.cs +++ b/src/Tests/DfE.ExternalApplications.Tests.Common/Seeders/EaContextSeeder.cs @@ -1,4 +1,5 @@ using DfE.CoreLibs.Contracts.ExternalApplications.Enums; +using DfE.ExternalApplications.Domain.Common; using DfE.ExternalApplications.Domain.Entities; using DfE.ExternalApplications.Domain.ValueObjects; using DfE.ExternalApplications.Infrastructure.Database; @@ -101,6 +102,7 @@ public static void SeedTestData(ExternalApplicationsContext ctx) userId: bobId, applicationId: applicationId, resourceKey: "Application:Read", + resourceType: ResourceType.Application, accessType: AccessType.Read, grantedOn: now, grantedBy: aliceId @@ -111,6 +113,7 @@ public static void SeedTestData(ExternalApplicationsContext ctx) userId: bobId, applicationId: applicationId, resourceKey: "Application:Write", + resourceType: ResourceType.Application, accessType: AccessType.Write, grantedOn: now, grantedBy: aliceId @@ -121,6 +124,7 @@ public static void SeedTestData(ExternalApplicationsContext ctx) userId: aliceId, applicationId: applicationId, resourceKey: "Application:Write", + resourceType: ResourceType.Application, accessType: AccessType.Write, grantedOn: now, grantedBy: aliceId @@ -128,6 +132,25 @@ public static void SeedTestData(ExternalApplicationsContext ctx) ctx.Permissions.AddRange(perm1, perm2, perm3); + var templatePerm1 = new TemplatePermission( + new TemplatePermissionId(Guid.NewGuid()), + userId: bobId, + templateId: templateId, + accessType: AccessType.Read, + grantedOn: now, + grantedBy: aliceId + ); + + var templatePerm2 = new TemplatePermission( + new TemplatePermissionId(Guid.NewGuid()), + userId: aliceId, + templateId: templateId, + accessType: AccessType.Write, + grantedOn: now, + grantedBy: aliceId + ); + ctx.TemplatePermissions.AddRange(templatePerm1, templatePerm2); + var response2Id = new ResponseId(Guid.NewGuid()); var response2 = new ApplicationResponse( response2Id, @@ -140,15 +163,7 @@ public static void SeedTestData(ExternalApplicationsContext ctx) ); ctx.ApplicationResponses.Add(response2); - var utaId = new UserTemplateAccessId(Guid.NewGuid()); - var uta = new UserTemplateAccess( - utaId, - userId: aliceId, - templateId: templateId, - grantedOn: now, - grantedBy: bobId - ); - ctx.UserTemplateAccesses.Add(uta); + ctx.SaveChanges(); } From 8c7affe2a173e0ff5dd881674af886b1459bd2ab Mon Sep 17 00:00:00 2001 From: FrostyApeOne Date: Fri, 13 Jun 2025 16:01:32 +0100 Subject: [PATCH 02/20] WIP - Added permission endpoints and Permission Claim Providers --- .../Generated/Client.g.cs | 14 +- .../Generated/Contracts.g.cs | 4 +- .../Generated/swagger.json | 41 +- .../Controllers/TemplatesController.cs | 19 +- .../DfE.ExternalApplications.Api.csproj | 2 +- ...fE.ExternalApplications.Application.csproj | 2 +- .../GetLatestTemplateSchemaQueryHandler.cs | 8 +- .../GetLatestTemplateSchemaQueryValidator.cs | 2 +- ...TemplateAccessByTemplateNameQueryObject.cs | 15 +- .../GetAllUserPermissionsQueryHandler.cs | 1 + ...tAllUserTemplatePermissionsQueryHandler.cs | 64 ++ ...erWithAllTemplatePermissionsQueryObject.cs | 22 + .../Common/ResourceType.cs | 11 - .../DfE.ExternalApplications.Domain.csproj | 2 +- .../Entities/User.cs | 14 +- .../20250602161455_Initial.Designer.cs | 597 ------------------ .../Migrations/20250602161455_Initial.cs | 499 --------------- ...xternalApplicationsContextModelSnapshot.cs | 594 ----------------- .../Authorization/PermissionClaimProvider.cs | 36 ++ .../TemplatePermissionClaimProvider.cs | 36 ++ .../Controllers/TemplatesControllerTests.cs | 4 +- ...etLatestTemplateSchemaQueryHandlerTests.cs | 17 +- .../GetUserPermissionsQueryHandlerTests.cs | 6 +- ...ateAccessByTemplateNameQueryObjectTests.cs | 55 -- ...tUserWithAllPermissionsQueryObjectTests.cs | 4 + ...LatestTemplateSchemaQueryValidatorTests.cs | 6 +- .../Aggregates/PermissionTests.cs | 4 + .../Entities/PermissionCustomization.cs | 1 + .../UserTemplateAccessCustomization.cs | 33 - 29 files changed, 259 insertions(+), 1854 deletions(-) create mode 100644 src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserTemplatePermissionsQueryHandler.cs create mode 100644 src/DfE.ExternalApplications.Application/Users/QueryObjects/GetUserWithAllTemplatePermissionsQueryObject.cs delete mode 100644 src/DfE.ExternalApplications.Domain/Common/ResourceType.cs delete mode 100644 src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.Designer.cs delete mode 100644 src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.cs delete mode 100644 src/DfE.ExternalApplications.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs create mode 100644 src/DfE.ExternalApplications.Infrastructure/Security/Authorization/PermissionClaimProvider.cs create mode 100644 src/DfE.ExternalApplications.Infrastructure/Security/Authorization/TemplatePermissionClaimProvider.cs delete mode 100644 src/Tests/DfE.ExternalApplications.Application.Tests/QueryObjects/Templates/GetUserTemplateAccessByTemplateNameQueryObjectTests.cs delete mode 100644 src/Tests/DfE.ExternalApplications.Tests.Common/Customizations/Entities/UserTemplateAccessCustomization.cs diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs b/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs index 60826708..f8c9284f 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs +++ b/src/DfE.ExternalApplications.Api.Client/Generated/Client.g.cs @@ -82,9 +82,9 @@ public string BaseUrl /// /// The latest template schema. /// A server side error occurred. - public virtual System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Guid userId) + public virtual System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName) { - return GetLatestTemplateSchemaAsync(templateName, userId, System.Threading.CancellationToken.None); + return GetLatestTemplateSchemaAsync(templateName, System.Threading.CancellationToken.None); } /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. @@ -93,14 +93,11 @@ public virtual System.Threading.Tasks.Task GetLatestTemplateS /// /// The latest template schema. /// A server side error occurred. - public virtual async System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Guid userId, System.Threading.CancellationToken cancellationToken) + public virtual async System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Threading.CancellationToken cancellationToken) { if (templateName == null) throw new System.ArgumentNullException("templateName"); - if (userId == null) - throw new System.ArgumentNullException("userId"); - var client_ = _httpClient; var disposeClient_ = false; try @@ -112,11 +109,10 @@ public virtual async System.Threading.Tasks.Task GetLatestTem var urlBuilder_ = new System.Text.StringBuilder(); if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl); - // Operation Path: "v1/Templates/{templateName}/schema/{userId}" + // Operation Path: "v1/Templates/{templateName}/schema" urlBuilder_.Append("v1/Templates/"); urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(templateName, System.Globalization.CultureInfo.InvariantCulture))); - urlBuilder_.Append("/schema/"); - urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(userId, System.Globalization.CultureInfo.InvariantCulture))); + urlBuilder_.Append("/schema"); PrepareRequest(client_, request_, urlBuilder_); diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs b/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs index 7a0f24c8..7902b285 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs +++ b/src/DfE.ExternalApplications.Api.Client/Generated/Contracts.g.cs @@ -33,7 +33,7 @@ public partial interface ITemplatesClient /// /// The latest template schema. /// A server side error occurred. - System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Guid userId); + System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName); /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// @@ -41,7 +41,7 @@ public partial interface ITemplatesClient /// /// The latest template schema. /// A server side error occurred. - System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Guid userId, System.Threading.CancellationToken cancellationToken); + System.Threading.Tasks.Task GetLatestTemplateSchemaAsync(string templateName, System.Threading.CancellationToken cancellationToken); } diff --git a/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json b/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json index c4a3c987..52305553 100644 --- a/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json +++ b/src/DfE.ExternalApplications.Api.Client/Generated/swagger.json @@ -6,7 +6,7 @@ "version": "1.0.0" }, "paths": { - "/v1/Templates/{templateName}/schema/{userId}": { + "/v1/Templates/{templateName}/schema": { "get": { "tags": [ "Templates" @@ -22,16 +22,6 @@ "type": "string" }, "x-position": 1 - }, - { - "name": "userId", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "guid" - }, - "x-position": 2 } ], "responses": { @@ -123,15 +113,15 @@ "type": "object", "additionalProperties": false, "properties": { + "templateId": { + "type": "string", + "format": "guid" + }, "versionNumber": { "type": "string" }, "jsonSchema": { "type": "string" - }, - "templateId": { - "type": "string", - "format": "guid" } } }, @@ -146,11 +136,32 @@ "resourceKey": { "type": "string" }, + "resourceType": { + "$ref": "#/components/schemas/ResourceType" + }, "accessType": { "$ref": "#/components/schemas/AccessType" } } }, + "ResourceType": { + "type": "string", + "description": "", + "x-enumNames": [ + "Application", + "TaskGroup", + "Task", + "Page", + "Field" + ], + "enum": [ + "Application", + "TaskGroup", + "Task", + "Page", + "Field" + ] + }, "AccessType": { "type": "string", "description": "", diff --git a/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs b/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs index 4548e144..01678a73 100644 --- a/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs +++ b/src/DfE.ExternalApplications.Api/Controllers/TemplatesController.cs @@ -1,6 +1,8 @@ +using System.Security.Claims; using Asp.Versioning; using DfE.CoreLibs.Contracts.ExternalApplications.Models.Response; using DfE.ExternalApplications.Application.Templates.Queries; +using DfE.ExternalApplications.Infrastructure.Security; using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -16,21 +18,22 @@ public class TemplatesController(ISender sender) : ControllerBase /// /// Returns the latest template schema for the specified template name if the user has access. /// - [HttpGet("{templateName}/schema/{userId}")] + [HttpGet("{templateName}/schema")] [SwaggerResponse(200, "The latest template schema.", typeof(TemplateSchemaDto))] [SwaggerResponse(400, "Request was invalid or access denied.")] - [Authorize(Policy = "CanRead")] - [Authorize(Policy = "CanRead")] + [Authorize(AuthenticationSchemes = AuthConstants.UserScheme, Policy = "CanReadTemplate")] + [Authorize(AuthenticationSchemes = AuthConstants.AzureAdScheme, Policy = "CanRead")] public async Task GetLatestTemplateSchemaAsync( - [FromRoute] string templateName, - [FromRoute] Guid userId, - CancellationToken cancellationToken) + [FromRoute] string templateName, CancellationToken cancellationToken) { - var query = new GetLatestTemplateSchemaQuery(templateName, userId); + var email = User.FindFirstValue(ClaimTypes.Email) + ?? throw new InvalidOperationException("No email claim in token"); + + var query = new GetLatestTemplateSchemaQuery(templateName, email); var result = await sender.Send(query, cancellationToken); if (!result.IsSuccess) - return BadRequest((object?)result.Error); + return BadRequest(result.Error); return Ok(result.Value); } diff --git a/src/DfE.ExternalApplications.Api/DfE.ExternalApplications.Api.csproj b/src/DfE.ExternalApplications.Api/DfE.ExternalApplications.Api.csproj index b6d9c16e..0cf3e6be 100644 --- a/src/DfE.ExternalApplications.Api/DfE.ExternalApplications.Api.csproj +++ b/src/DfE.ExternalApplications.Api/DfE.ExternalApplications.Api.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj b/src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj index ceee3d4e..e2a9acf9 100644 --- a/src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj +++ b/src/DfE.ExternalApplications.Application/DfE.ExternalApplications.Application.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryHandler.cs b/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryHandler.cs index bc522e30..52dea3ce 100644 --- a/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryHandler.cs +++ b/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryHandler.cs @@ -9,11 +9,11 @@ namespace DfE.ExternalApplications.Application.Templates.Queries; -public sealed record GetLatestTemplateSchemaQuery(string TemplateName, Guid UserId) +public sealed record GetLatestTemplateSchemaQuery(string TemplateName, string Email) : IRequest>; public sealed class GetLatestTemplateSchemaQueryHandler( - IEaRepository accessRepo, + IEaRepository accessRepo, IEaRepository versionRepo, ICacheService cacheService) : IRequestHandler> @@ -24,7 +24,7 @@ public async Task> Handle( { try { - var cacheKey = $"TemplateSchema_{CacheKeyHelper.GenerateHashedCacheKey(request.TemplateName)}_{request.UserId}"; + var cacheKey = $"TemplateSchema_{CacheKeyHelper.GenerateHashedCacheKey(request.TemplateName)}_{request.Email}"; var methodName = nameof(GetLatestTemplateSchemaQueryHandler); @@ -32,7 +32,7 @@ public async Task> Handle( cacheKey, async () => { - var access = await new GetUserTemplateAccessByTemplateNameQueryObject(request.UserId, request.TemplateName) + var access = await new GetTemplatePermissionByTemplateNameQueryObject(request.Email, request.TemplateName) .Apply(accessRepo.Query().AsNoTracking()) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryValidator.cs b/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryValidator.cs index 209ee2e8..f6460644 100644 --- a/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryValidator.cs +++ b/src/DfE.ExternalApplications.Application/Templates/Queries/GetLatestTemplateSchemaQueryValidator.cs @@ -10,7 +10,7 @@ public GetLatestTemplateSchemaQueryValidator() { RuleFor(x => x.TemplateName) .NotEmpty(); - RuleFor(x => x.UserId) + RuleFor(x => x.Email) .NotEmpty(); } } diff --git a/src/DfE.ExternalApplications.Application/Templates/QueryObjects/GetUserTemplateAccessByTemplateNameQueryObject.cs b/src/DfE.ExternalApplications.Application/Templates/QueryObjects/GetUserTemplateAccessByTemplateNameQueryObject.cs index a55b5974..c3fbf0d0 100644 --- a/src/DfE.ExternalApplications.Application/Templates/QueryObjects/GetUserTemplateAccessByTemplateNameQueryObject.cs +++ b/src/DfE.ExternalApplications.Application/Templates/QueryObjects/GetUserTemplateAccessByTemplateNameQueryObject.cs @@ -1,18 +1,21 @@ using DfE.ExternalApplications.Application.Common.QueriesObjects; using DfE.ExternalApplications.Domain.Entities; -using DfE.ExternalApplications.Domain.ValueObjects; using Microsoft.EntityFrameworkCore; namespace DfE.ExternalApplications.Application.Templates.QueryObjects; -public sealed class GetUserTemplateAccessByTemplateNameQueryObject(Guid userId, string templateName) - : IQueryObject +public sealed class GetTemplatePermissionByTemplateNameQueryObject(string email, string templateName) + : IQueryObject { - private readonly UserId _userId = new(userId); + private readonly string _normalizedEmail = email.Trim().ToLowerInvariant(); private readonly string _normalizedName = templateName.Trim().ToLowerInvariant(); - public IQueryable Apply(IQueryable query) => + public IQueryable Apply(IQueryable query) => query .Include(x => x.Template) - .Where(x => x.UserId == _userId && x.Template!.Name.ToLower() == _normalizedName); + .Include(x => x.User) + .Where(x => + x.User != null + && x.User.Email.Equals(_normalizedEmail, StringComparison.InvariantCultureIgnoreCase) + && x.Template!.Name.ToLower() == _normalizedName); } diff --git a/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserPermissionsQueryHandler.cs b/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserPermissionsQueryHandler.cs index aae1eb1b..4728b85c 100644 --- a/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserPermissionsQueryHandler.cs +++ b/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserPermissionsQueryHandler.cs @@ -45,6 +45,7 @@ public async Task>> Handle( .Select(p => new UserPermissionDto { ApplicationId = p.ApplicationId.Value, + ResourceType = p.ResourceType, ResourceKey = p.ResourceKey, AccessType = p.AccessType }) diff --git a/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserTemplatePermissionsQueryHandler.cs b/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserTemplatePermissionsQueryHandler.cs new file mode 100644 index 00000000..4ec55fa0 --- /dev/null +++ b/src/DfE.ExternalApplications.Application/Users/Queries/GetAllUserTemplatePermissionsQueryHandler.cs @@ -0,0 +1,64 @@ +using DfE.CoreLibs.Caching.Helpers; +using DfE.CoreLibs.Caching.Interfaces; +using DfE.CoreLibs.Contracts.ExternalApplications.Models.Response; +using DfE.ExternalApplications.Application.Users.QueryObjects; +using DfE.ExternalApplications.Domain.Entities; +using DfE.ExternalApplications.Domain.Interfaces.Repositories; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace DfE.ExternalApplications.Application.Users.Queries +{ + public sealed record GetAllUserTemplatePermissionsQuery(string Email) + : IRequest>>; + + public sealed class GetAllUserTemplatePermissionsQueryHandler( + IEaRepository userRepo, + ICacheService cacheService) + : IRequestHandler>> + { + public async Task>> Handle( + GetAllUserTemplatePermissionsQuery request, + CancellationToken cancellationToken) + { + try + { + var cacheKey = $"Template_Permissions_{CacheKeyHelper.GenerateHashedCacheKey(request.Email)}"; + + var methodName = nameof(GetAllUserTemplatePermissionsQueryHandler); + + return await cacheService.GetOrAddAsync( + cacheKey, + async () => + { + var userWithTemplatePermissions = await new GetUserWithAllTemplatePermissionsQueryObject(request.Email) + .Apply(userRepo.Query().AsNoTracking()) + .FirstOrDefaultAsync(cancellationToken); + + if (userWithTemplatePermissions is null) + { + return Result>.Success(Array.Empty()); + } + + var dtoList = userWithTemplatePermissions.TemplatePermissions + .Select(p => new TemplatePermissionDto + { + TemplatePermissionId = p.Id?.Value, + UserId = p.UserId.Value, + TemplateId = p.TemplateId.Value, + AccessType = p.AccessType + }) + .ToList() + .AsReadOnly(); + + return Result>.Success(dtoList); + }, + methodName); + } + catch (Exception e) + { + return Result>.Failure(e.ToString()); + } + } + } +} \ No newline at end of file diff --git a/src/DfE.ExternalApplications.Application/Users/QueryObjects/GetUserWithAllTemplatePermissionsQueryObject.cs b/src/DfE.ExternalApplications.Application/Users/QueryObjects/GetUserWithAllTemplatePermissionsQueryObject.cs new file mode 100644 index 00000000..0c41661f --- /dev/null +++ b/src/DfE.ExternalApplications.Application/Users/QueryObjects/GetUserWithAllTemplatePermissionsQueryObject.cs @@ -0,0 +1,22 @@ +using DfE.ExternalApplications.Application.Common.QueriesObjects; +using DfE.ExternalApplications.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace DfE.ExternalApplications.Application.Users.QueryObjects +{ + /// + /// Filters to one user by normalized email, and includes all Template Permission children. + /// + public sealed class GetUserWithAllTemplatePermissionsQueryObject(string email) + : IQueryObject + { + private readonly string _normalizedEmail = email.Trim().ToLowerInvariant(); + + public IQueryable Apply(IQueryable query) + { + return query + .Where(u => u.Email.ToLower() == _normalizedEmail) + .Include(u => u.TemplatePermissions); + } + } +} \ No newline at end of file diff --git a/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs b/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs deleted file mode 100644 index 20ff9a9e..00000000 --- a/src/DfE.ExternalApplications.Domain/Common/ResourceType.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DfE.ExternalApplications.Domain.Common -{ - public enum ResourceType - { - Application, - TaskGroup, - Task, - Page, - Field - } -} diff --git a/src/DfE.ExternalApplications.Domain/DfE.ExternalApplications.Domain.csproj b/src/DfE.ExternalApplications.Domain/DfE.ExternalApplications.Domain.csproj index 31cb1b46..c15875f3 100644 --- a/src/DfE.ExternalApplications.Domain/DfE.ExternalApplications.Domain.csproj +++ b/src/DfE.ExternalApplications.Domain/DfE.ExternalApplications.Domain.csproj @@ -6,7 +6,7 @@ - + diff --git a/src/DfE.ExternalApplications.Domain/Entities/User.cs b/src/DfE.ExternalApplications.Domain/Entities/User.cs index 4fa2304b..144b43bf 100644 --- a/src/DfE.ExternalApplications.Domain/Entities/User.cs +++ b/src/DfE.ExternalApplications.Domain/Entities/User.cs @@ -20,10 +20,14 @@ public sealed class User : BaseAggregateRoot, IEntity public User? LastModifiedByUser { get; private set; } private readonly List _permissions = new(); + private readonly List _templatePermissions = new(); public IReadOnlyCollection Permissions => _permissions.AsReadOnly(); + public IReadOnlyCollection TemplatePermissions + => _templatePermissions.AsReadOnly(); + private User() { // Required by EF Core to materialise the entity. @@ -42,7 +46,8 @@ public User( UserId? createdBy, DateTime? lastModifiedOn, UserId? lastModifiedBy, - IEnumerable? initialPermissions = null) + IEnumerable? initialPermissions = null, + IEnumerable? initialTemplatePermissions = null) { Id = id ?? throw new ArgumentNullException(nameof(id)); RoleId = roleId ?? throw new ArgumentNullException(nameof(roleId)); @@ -57,6 +62,11 @@ public User( { _permissions.AddRange(initialPermissions); } + + if (initialTemplatePermissions != null) + { + _templatePermissions.AddRange(initialTemplatePermissions); + } } /// @@ -65,6 +75,7 @@ public User( public Permission AddPermission( ApplicationId applicationId, string resourceKey, + ResourceType resourceType, AccessType accessType, UserId grantedBy, DateTime? grantedOn = null) @@ -80,6 +91,7 @@ public Permission AddPermission( this.Id ?? throw new InvalidOperationException("UserId must be set before adding a permission."), applicationId, resourceKey, + resourceType, accessType, when, grantedBy); diff --git a/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.Designer.cs b/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.Designer.cs deleted file mode 100644 index 3c32594d..00000000 --- a/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.Designer.cs +++ /dev/null @@ -1,597 +0,0 @@ -// -using System; -using DfE.ExternalApplications.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace DfE.ExternalApplications.Infrastructure.Migrations -{ - [DbContext(typeof(ExternalApplicationsContext))] - [Migration("20250602161455_Initial")] - partial class Initial - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.16") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Application", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("ApplicationReference") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("nvarchar(20)") - .HasColumnName("ApplicationReference"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("Status") - .HasColumnType("int") - .HasColumnName("Status"); - - b.Property("TemplateVersionId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateVersionId"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("TemplateVersionId"); - - b.ToTable("Applications", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.ApplicationResponse", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("ResponseId"); - - b.Property("ApplicationId") - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("ResponseBody") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ResponseBody"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.ToTable("ApplicationResponses", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("PermissionId"); - - b.Property("AccessType") - .HasColumnType("tinyint") - .HasColumnName("AccessType"); - - b.Property("ApplicationId") - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("GrantedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("GrantedBy"); - - b.Property("GrantedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("GrantedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("ResourceKey") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("nvarchar(200)") - .HasColumnName("ResourceKey"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("GrantedBy"); - - b.HasIndex("UserId"); - - b.ToTable("Permissions", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Role", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("RoleId"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)") - .HasColumnName("Name"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Roles", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TaskAssignmentLabel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TaskAssignmentLabelsId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("TaskId") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("nvarchar(10)") - .HasColumnName("TaskId"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Value"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("UserId"); - - b.ToTable("TaskAssignmentLabels", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Template", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Name"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.ToTable("Templates", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TemplateVersion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateVersionId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("JsonSchema") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("JsonSchema"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("TemplateId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("VersionNumber") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)") - .HasColumnName("VersionNumber"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("TemplateId"); - - b.ToTable("TemplateVersions", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)") - .HasColumnName("Email"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Name"); - - b.Property("RoleId") - .HasColumnType("uniqueidentifier") - .HasColumnName("RoleId"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("RoleId"); - - b.ToTable("Users", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.UserTemplateAccess", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("UserTemplateAccessId"); - - b.Property("GrantedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("GrantedBy"); - - b.Property("GrantedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("GrantedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("TemplateId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.HasKey("Id"); - - b.HasIndex("GrantedBy"); - - b.HasIndex("TemplateId"); - - b.HasIndex("UserId"); - - b.ToTable("UserTemplateAccess", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Application", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.TemplateVersion", "TemplateVersion") - .WithMany() - .HasForeignKey("TemplateVersionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("TemplateVersion"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.ApplicationResponse", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("Application"); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Permission", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "GrantedByUser") - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "User") - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Application"); - - b.Navigation("GrantedByUser"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TaskAssignmentLabel", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "AssignedUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("AssignedUser"); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Template", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TemplateVersion", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Template", "Template") - .WithMany() - .HasForeignKey("TemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("Template"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Role", "Role") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("Role"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.UserTemplateAccess", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "GrantedByUser") - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Template", "Template") - .WithMany() - .HasForeignKey("TemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GrantedByUser"); - - b.Navigation("Template"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.Navigation("Permissions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.cs b/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.cs deleted file mode 100644 index bf7dc070..00000000 --- a/src/DfE.ExternalApplications.Infrastructure/Migrations/20250602161455_Initial.cs +++ /dev/null @@ -1,499 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace DfE.ExternalApplications.Infrastructure.Migrations -{ - /// - public partial class Initial : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.EnsureSchema( - name: "ea"); - - migrationBuilder.CreateTable( - name: "Roles", - schema: "ea", - columns: table => new - { - RoleId = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Roles", x => x.RoleId); - }); - - migrationBuilder.CreateTable( - name: "Users", - schema: "ea", - columns: table => new - { - UserId = table.Column(type: "uniqueidentifier", nullable: false), - RoleId = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: true), - LastModifiedOn = table.Column(type: "datetime2", nullable: true), - LastModifiedBy = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.UserId); - table.ForeignKey( - name: "FK_Users_Roles_RoleId", - column: x => x.RoleId, - principalSchema: "ea", - principalTable: "Roles", - principalColumn: "RoleId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Users_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Users_Users_LastModifiedBy", - column: x => x.LastModifiedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "TaskAssignmentLabels", - schema: "ea", - columns: table => new - { - TaskAssignmentLabelsId = table.Column(type: "uniqueidentifier", nullable: false), - Value = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - TaskId = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), - UserId = table.Column(type: "uniqueidentifier", nullable: true), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TaskAssignmentLabels", x => x.TaskAssignmentLabelsId); - table.ForeignKey( - name: "FK_TaskAssignmentLabels_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_TaskAssignmentLabels_Users_UserId", - column: x => x.UserId, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Templates", - schema: "ea", - columns: table => new - { - TemplateId = table.Column(type: "uniqueidentifier", nullable: false), - Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Templates", x => x.TemplateId); - table.ForeignKey( - name: "FK_Templates_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "TemplateVersions", - schema: "ea", - columns: table => new - { - TemplateVersionId = table.Column(type: "uniqueidentifier", nullable: false), - TemplateId = table.Column(type: "uniqueidentifier", nullable: false), - VersionNumber = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), - JsonSchema = table.Column(type: "nvarchar(max)", nullable: false), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: false), - LastModifiedOn = table.Column(type: "datetime2", nullable: true), - LastModifiedBy = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_TemplateVersions", x => x.TemplateVersionId); - table.ForeignKey( - name: "FK_TemplateVersions_Templates_TemplateId", - column: x => x.TemplateId, - principalSchema: "ea", - principalTable: "Templates", - principalColumn: "TemplateId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TemplateVersions_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_TemplateVersions_Users_LastModifiedBy", - column: x => x.LastModifiedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "UserTemplateAccess", - schema: "ea", - columns: table => new - { - UserTemplateAccessId = table.Column(type: "uniqueidentifier", nullable: false), - UserId = table.Column(type: "uniqueidentifier", nullable: false), - TemplateId = table.Column(type: "uniqueidentifier", nullable: false), - GrantedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - GrantedBy = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_UserTemplateAccess", x => x.UserTemplateAccessId); - table.ForeignKey( - name: "FK_UserTemplateAccess_Templates_TemplateId", - column: x => x.TemplateId, - principalSchema: "ea", - principalTable: "Templates", - principalColumn: "TemplateId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_UserTemplateAccess_Users_GrantedBy", - column: x => x.GrantedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_UserTemplateAccess_Users_UserId", - column: x => x.UserId, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "Applications", - schema: "ea", - columns: table => new - { - ApplicationId = table.Column(type: "uniqueidentifier", nullable: false), - ApplicationReference = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), - TemplateVersionId = table.Column(type: "uniqueidentifier", nullable: false), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: false), - Status = table.Column(type: "int", nullable: true), - LastModifiedOn = table.Column(type: "datetime2", nullable: true), - LastModifiedBy = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_Applications", x => x.ApplicationId); - table.ForeignKey( - name: "FK_Applications_TemplateVersions_TemplateVersionId", - column: x => x.TemplateVersionId, - principalSchema: "ea", - principalTable: "TemplateVersions", - principalColumn: "TemplateVersionId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Applications_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Applications_Users_LastModifiedBy", - column: x => x.LastModifiedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "ApplicationResponses", - schema: "ea", - columns: table => new - { - ResponseId = table.Column(type: "uniqueidentifier", nullable: false), - ApplicationId = table.Column(type: "uniqueidentifier", nullable: false), - ResponseBody = table.Column(type: "nvarchar(max)", nullable: false), - CreatedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - CreatedBy = table.Column(type: "uniqueidentifier", nullable: false), - LastModifiedOn = table.Column(type: "datetime2", nullable: true), - LastModifiedBy = table.Column(type: "uniqueidentifier", nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_ApplicationResponses", x => x.ResponseId); - table.ForeignKey( - name: "FK_ApplicationResponses_Applications_ApplicationId", - column: x => x.ApplicationId, - principalSchema: "ea", - principalTable: "Applications", - principalColumn: "ApplicationId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_ApplicationResponses_Users_CreatedBy", - column: x => x.CreatedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_ApplicationResponses_Users_LastModifiedBy", - column: x => x.LastModifiedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - }); - - migrationBuilder.CreateTable( - name: "Permissions", - schema: "ea", - columns: table => new - { - PermissionId = table.Column(type: "uniqueidentifier", nullable: false), - UserId = table.Column(type: "uniqueidentifier", nullable: false), - ApplicationId = table.Column(type: "uniqueidentifier", nullable: false), - ResourceKey = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), - AccessType = table.Column(type: "tinyint", nullable: false), - GrantedOn = table.Column(type: "datetime2", nullable: false, defaultValueSql: "GETDATE()"), - GrantedBy = table.Column(type: "uniqueidentifier", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_Permissions", x => x.PermissionId); - table.ForeignKey( - name: "FK_Permissions_Applications_ApplicationId", - column: x => x.ApplicationId, - principalSchema: "ea", - principalTable: "Applications", - principalColumn: "ApplicationId", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_Permissions_Users_GrantedBy", - column: x => x.GrantedBy, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Restrict); - table.ForeignKey( - name: "FK_Permissions_Users_UserId", - column: x => x.UserId, - principalSchema: "ea", - principalTable: "Users", - principalColumn: "UserId", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_ApplicationResponses_ApplicationId", - schema: "ea", - table: "ApplicationResponses", - column: "ApplicationId"); - - migrationBuilder.CreateIndex( - name: "IX_ApplicationResponses_CreatedBy", - schema: "ea", - table: "ApplicationResponses", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_ApplicationResponses_LastModifiedBy", - schema: "ea", - table: "ApplicationResponses", - column: "LastModifiedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Applications_CreatedBy", - schema: "ea", - table: "Applications", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Applications_LastModifiedBy", - schema: "ea", - table: "Applications", - column: "LastModifiedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Applications_TemplateVersionId", - schema: "ea", - table: "Applications", - column: "TemplateVersionId"); - - migrationBuilder.CreateIndex( - name: "IX_Permissions_ApplicationId", - schema: "ea", - table: "Permissions", - column: "ApplicationId"); - - migrationBuilder.CreateIndex( - name: "IX_Permissions_GrantedBy", - schema: "ea", - table: "Permissions", - column: "GrantedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Permissions_UserId", - schema: "ea", - table: "Permissions", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Roles_Name", - schema: "ea", - table: "Roles", - column: "Name", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_TaskAssignmentLabels_CreatedBy", - schema: "ea", - table: "TaskAssignmentLabels", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_TaskAssignmentLabels_UserId", - schema: "ea", - table: "TaskAssignmentLabels", - column: "UserId"); - - migrationBuilder.CreateIndex( - name: "IX_Templates_CreatedBy", - schema: "ea", - table: "Templates", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_TemplateVersions_CreatedBy", - schema: "ea", - table: "TemplateVersions", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_TemplateVersions_LastModifiedBy", - schema: "ea", - table: "TemplateVersions", - column: "LastModifiedBy"); - - migrationBuilder.CreateIndex( - name: "IX_TemplateVersions_TemplateId", - schema: "ea", - table: "TemplateVersions", - column: "TemplateId"); - - migrationBuilder.CreateIndex( - name: "IX_Users_CreatedBy", - schema: "ea", - table: "Users", - column: "CreatedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Users_Email", - schema: "ea", - table: "Users", - column: "Email", - unique: true); - - migrationBuilder.CreateIndex( - name: "IX_Users_LastModifiedBy", - schema: "ea", - table: "Users", - column: "LastModifiedBy"); - - migrationBuilder.CreateIndex( - name: "IX_Users_RoleId", - schema: "ea", - table: "Users", - column: "RoleId"); - - migrationBuilder.CreateIndex( - name: "IX_UserTemplateAccess_GrantedBy", - schema: "ea", - table: "UserTemplateAccess", - column: "GrantedBy"); - - migrationBuilder.CreateIndex( - name: "IX_UserTemplateAccess_TemplateId", - schema: "ea", - table: "UserTemplateAccess", - column: "TemplateId"); - - migrationBuilder.CreateIndex( - name: "IX_UserTemplateAccess_UserId", - schema: "ea", - table: "UserTemplateAccess", - column: "UserId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "ApplicationResponses", - schema: "ea"); - - migrationBuilder.DropTable( - name: "Permissions", - schema: "ea"); - - migrationBuilder.DropTable( - name: "TaskAssignmentLabels", - schema: "ea"); - - migrationBuilder.DropTable( - name: "UserTemplateAccess", - schema: "ea"); - - migrationBuilder.DropTable( - name: "Applications", - schema: "ea"); - - migrationBuilder.DropTable( - name: "TemplateVersions", - schema: "ea"); - - migrationBuilder.DropTable( - name: "Templates", - schema: "ea"); - - migrationBuilder.DropTable( - name: "Users", - schema: "ea"); - - migrationBuilder.DropTable( - name: "Roles", - schema: "ea"); - } - } -} diff --git a/src/DfE.ExternalApplications.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs b/src/DfE.ExternalApplications.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs deleted file mode 100644 index 5875431b..00000000 --- a/src/DfE.ExternalApplications.Infrastructure/Migrations/ExternalApplicationsContextModelSnapshot.cs +++ /dev/null @@ -1,594 +0,0 @@ -// -using System; -using DfE.ExternalApplications.Infrastructure.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace DfE.ExternalApplications.Infrastructure.Migrations -{ - [DbContext(typeof(ExternalApplicationsContext))] - partial class ExternalApplicationsContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.16") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Application", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("ApplicationReference") - .IsRequired() - .HasMaxLength(20) - .HasColumnType("nvarchar(20)") - .HasColumnName("ApplicationReference"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("Status") - .HasColumnType("int") - .HasColumnName("Status"); - - b.Property("TemplateVersionId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateVersionId"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("TemplateVersionId"); - - b.ToTable("Applications", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.ApplicationResponse", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("ResponseId"); - - b.Property("ApplicationId") - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("ResponseBody") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("ResponseBody"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.ToTable("ApplicationResponses", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Permission", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("PermissionId"); - - b.Property("AccessType") - .HasColumnType("tinyint") - .HasColumnName("AccessType"); - - b.Property("ApplicationId") - .HasColumnType("uniqueidentifier") - .HasColumnName("ApplicationId"); - - b.Property("GrantedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("GrantedBy"); - - b.Property("GrantedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("GrantedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("ResourceKey") - .IsRequired() - .HasMaxLength(200) - .HasColumnType("nvarchar(200)") - .HasColumnName("ResourceKey"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId"); - - b.HasIndex("GrantedBy"); - - b.HasIndex("UserId"); - - b.ToTable("Permissions", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Role", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("RoleId"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)") - .HasColumnName("Name"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("Roles", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TaskAssignmentLabel", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TaskAssignmentLabelsId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("TaskId") - .IsRequired() - .HasMaxLength(10) - .HasColumnType("nvarchar(10)") - .HasColumnName("TaskId"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.Property("Value") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Value"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("UserId"); - - b.ToTable("TaskAssignmentLabels", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Template", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Name"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.ToTable("Templates", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TemplateVersion", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateVersionId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("JsonSchema") - .IsRequired() - .HasColumnType("nvarchar(max)") - .HasColumnName("JsonSchema"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("TemplateId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("VersionNumber") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("nvarchar(50)") - .HasColumnName("VersionNumber"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("TemplateId"); - - b.ToTable("TemplateVersions", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.Property("CreatedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("CreatedBy"); - - b.Property("CreatedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("CreatedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("Email") - .IsRequired() - .HasMaxLength(256) - .HasColumnType("nvarchar(256)") - .HasColumnName("Email"); - - b.Property("LastModifiedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("LastModifiedBy"); - - b.Property("LastModifiedOn") - .HasColumnType("datetime2") - .HasColumnName("LastModifiedOn"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("nvarchar(100)") - .HasColumnName("Name"); - - b.Property("RoleId") - .HasColumnType("uniqueidentifier") - .HasColumnName("RoleId"); - - b.HasKey("Id"); - - b.HasIndex("CreatedBy"); - - b.HasIndex("Email") - .IsUnique(); - - b.HasIndex("LastModifiedBy"); - - b.HasIndex("RoleId"); - - b.ToTable("Users", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.UserTemplateAccess", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasColumnName("UserTemplateAccessId"); - - b.Property("GrantedBy") - .HasColumnType("uniqueidentifier") - .HasColumnName("GrantedBy"); - - b.Property("GrantedOn") - .ValueGeneratedOnAdd() - .HasColumnType("datetime2") - .HasColumnName("GrantedOn") - .HasDefaultValueSql("GETDATE()"); - - b.Property("TemplateId") - .HasColumnType("uniqueidentifier") - .HasColumnName("TemplateId"); - - b.Property("UserId") - .HasColumnType("uniqueidentifier") - .HasColumnName("UserId"); - - b.HasKey("Id"); - - b.HasIndex("GrantedBy"); - - b.HasIndex("TemplateId"); - - b.HasIndex("UserId"); - - b.ToTable("UserTemplateAccess", "ea"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Application", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.TemplateVersion", "TemplateVersion") - .WithMany() - .HasForeignKey("TemplateVersionId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("TemplateVersion"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.ApplicationResponse", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("Application"); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Permission", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "GrantedByUser") - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "User") - .WithMany("Permissions") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Application"); - - b.Navigation("GrantedByUser"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TaskAssignmentLabel", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "AssignedUser") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Restrict); - - b.Navigation("AssignedUser"); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.Template", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.Navigation("CreatedByUser"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.TemplateVersion", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Template", "Template") - .WithMany() - .HasForeignKey("TemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("Template"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "CreatedByUser") - .WithMany() - .HasForeignKey("CreatedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "LastModifiedByUser") - .WithMany() - .HasForeignKey("LastModifiedBy") - .OnDelete(DeleteBehavior.Restrict); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Role", "Role") - .WithMany() - .HasForeignKey("RoleId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("CreatedByUser"); - - b.Navigation("LastModifiedByUser"); - - b.Navigation("Role"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.UserTemplateAccess", b => - { - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "GrantedByUser") - .WithMany() - .HasForeignKey("GrantedBy") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.Template", "Template") - .WithMany() - .HasForeignKey("TemplateId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("DfE.ExternalApplications.Domain.Entities.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("GrantedByUser"); - - b.Navigation("Template"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("DfE.ExternalApplications.Domain.Entities.User", b => - { - b.Navigation("Permissions"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/PermissionClaimProvider.cs b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/PermissionClaimProvider.cs new file mode 100644 index 00000000..6fb9c6b8 --- /dev/null +++ b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/PermissionClaimProvider.cs @@ -0,0 +1,36 @@ +using DfE.CoreLibs.Security.Interfaces; +using DfE.ExternalApplications.Application.Users.Queries; +using MediatR; +using Microsoft.Extensions.Logging; +using System.Security.Claims; + +namespace DfE.ExternalApplications.Infrastructure.Security.Authorization +{ + public class PermissionsClaimProvider(ISender sender, ILogger logger) : ICustomClaimProvider + { + public async Task> GetClaimsAsync(ClaimsPrincipal principal) + { + var email = principal.FindFirstValue(ClaimTypes.Email); + if (string.IsNullOrEmpty(email)) + return Array.Empty(); + + var query = new GetAllUserPermissionsQuery(email); + var result = await sender.Send(query); + + if (result is { IsSuccess: false }) + { + logger.LogWarning($"PermissionsClaimProvider() > Failed to return the user permissions for {email}"); + return Array.Empty(); + } + + return result.Value == null ? + Array.Empty() : + result.Value.Select(p => + new Claim( + "permission", + $"{p.ResourceType}:{p.ResourceKey}:{p.AccessType}" + ) + ); + } + } +} diff --git a/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/TemplatePermissionClaimProvider.cs b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/TemplatePermissionClaimProvider.cs new file mode 100644 index 00000000..d3aa3b7a --- /dev/null +++ b/src/DfE.ExternalApplications.Infrastructure/Security/Authorization/TemplatePermissionClaimProvider.cs @@ -0,0 +1,36 @@ +using DfE.CoreLibs.Security.Interfaces; +using DfE.ExternalApplications.Application.Users.Queries; +using MediatR; +using Microsoft.Extensions.Logging; +using System.Security.Claims; + +namespace DfE.ExternalApplications.Infrastructure.Security.Authorization +{ + public class TemplatePermissionsClaimProvider(ISender sender, ILogger logger) : ICustomClaimProvider + { + public async Task> GetClaimsAsync(ClaimsPrincipal principal) + { + var email = principal.FindFirstValue(ClaimTypes.Email); + if (string.IsNullOrEmpty(email)) + return Array.Empty(); + + var query = new GetAllUserTemplatePermissionsQuery(email); + var result = await sender.Send(query); + + if (result is { IsSuccess: false }) + { + logger.LogWarning($"TemplatePermissionsClaimProvider() > Failed to return the template permissions for user: {email}"); + return Array.Empty(); + } + + return result.Value == null ? + Array.Empty() : + result.Value.Select(p => + new Claim( + "permission", + $"Template:{p.TemplateId}:{p.AccessType.ToString()}" + ) + ); + } + } +} diff --git a/src/Tests/DfE.ExternalApplications.Api.Tests.Integration/Controllers/TemplatesControllerTests.cs b/src/Tests/DfE.ExternalApplications.Api.Tests.Integration/Controllers/TemplatesControllerTests.cs index 5b4309bb..e1bd95bd 100644 --- a/src/Tests/DfE.ExternalApplications.Api.Tests.Integration/Controllers/TemplatesControllerTests.cs +++ b/src/Tests/DfE.ExternalApplications.Api.Tests.Integration/Controllers/TemplatesControllerTests.cs @@ -21,7 +21,7 @@ public async Task GetLatestTemplateSchemaAsync_ReturnsLatestSchema_WhenUserHasAc // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery var template = await dbContext.Templates.FirstAsync(); - var userAccess = await dbContext.UserTemplateAccesses.FirstAsync(); + var userAccess = await dbContext.TemplatePermissions.FirstAsync(); // add a newer version // ReSharper disable once EntityFramework.NPlusOne.IncompleteDataQuery @@ -40,7 +40,7 @@ public async Task GetLatestTemplateSchemaAsync_ReturnsLatestSchema_WhenUserHasAc dbContext.TemplateVersions.Add(newVersion); await dbContext.SaveChangesAsync(); - var response = await templatesClient.GetLatestTemplateSchemaAsync(template.Name, userAccess.UserId.Value); + var response = await templatesClient.GetLatestTemplateSchemaAsync(template.Name); Assert.NotNull(response); Assert.Equal("v9.9", response!.VersionNumber); diff --git a/src/Tests/DfE.ExternalApplications.Application.Tests/QueryHandlers/Templates/GetLatestTemplateSchemaQueryHandlerTests.cs b/src/Tests/DfE.ExternalApplications.Application.Tests/QueryHandlers/Templates/GetLatestTemplateSchemaQueryHandlerTests.cs index e216a581..ec6d9867 100644 --- a/src/Tests/DfE.ExternalApplications.Application.Tests/QueryHandlers/Templates/GetLatestTemplateSchemaQueryHandlerTests.cs +++ b/src/Tests/DfE.ExternalApplications.Application.Tests/QueryHandlers/Templates/GetLatestTemplateSchemaQueryHandlerTests.cs @@ -14,24 +14,25 @@ namespace DfE.ExternalApplications.Application.Tests.QueryHandlers.Templates; public class GetLatestTemplateSchemaQueryHandlerTests { - [Theory, CustomAutoData(typeof(UserTemplateAccessCustomization), typeof(TemplateVersionCustomization))] + [Theory, CustomAutoData(typeof(TemplatePermissionCustomization), typeof(TemplateVersionCustomization))] public async Task Handle_ShouldReturnLatestSchema_WhenUserHasAccess( - UserTemplateAccessCustomization utaCustom, + TemplatePermissionCustomization utaCustom, TemplateVersionCustomization tvCustom, - [Frozen] IEaRepository accessRepo, + [Frozen] IEaRepository accessRepo, [Frozen] IEaRepository versionRepo, [Frozen] ICacheService cacheService) { var template = new Fixture().Customize(new TemplateCustomization()).Create