From 4bda675baa58b058f3e80f9e6f3a94ab9c0b18c8 Mon Sep 17 00:00:00 2001 From: David Jimenez Date: Wed, 30 Oct 2024 13:20:03 -0600 Subject: [PATCH] Admin Console - Instances endpoint --- Application/Ed-Fi-ODS-AdminApi.sln | 13 ++- .../EdFi.Ods.AdminApi.AdminConsole.csproj | 3 +- .../Features/Instances/AddInstance.cs | 72 ++++++++++++++ .../Features/Instances/InstanceModel.cs | 26 ++++++ .../Features/Instances/ReadInstances.cs | 37 ++++++++ .../Helpers/Encryption.cs | 35 +++++++ .../Helpers/IEncryptionKeyResolver.cs | 11 +++ .../Helpers/IEncryptionKeySettings.cs | 11 +++ .../Helpers/OptionsEncryptionKeyResolver.cs | 21 +++++ .../MsSql/20241030014056_InitialCreate.cs | 7 +- .../20241030141215_InstanceTables.Designer.cs | 88 ++++++++++++++++++ .../MsSql/20241030141215_InstanceTables.cs | 65 +++++++++++++ .../AdminConsoleSqlContextModelSnapshot.cs | 26 ++++++ .../AdminConsolePg/AdminConsolePgContext.cs | 3 + .../AdminConsoleSql/AdminConsoleSqlContext.cs | 3 + .../DataAccess/Contexts/IDbContext.cs | 10 +- .../InstanceConfiguration.cs | 37 ++++++++ .../DataAccess/Models/IModel.cs | 16 ++++ .../DataAccess/Models/Instance.cs | 15 +++ .../Services/EncryptionService.cs | 70 ++++++++++++++ .../Instances/Commands/AddInstanceCommand.cs | 79 ++++++++++++++++ .../Instances/Queries/GetInstanceQuery.cs | 62 +++++++++++++ .../Instances/Queries/GetInstancesQuery.cs | 58 ++++++++++++ .../Services/ServiceRegistration.cs | 17 ++++ .../Helpers/AdminConsoleSettings.cs | 13 +++ Application/EdFi.Ods.AdminApi/Program.cs | 13 +++ .../EdFi.Ods.AdminApi/appsettings.json | 7 +- .../AssertionExtensions.cs | 69 ++++++++++++++ .../CommandTests/AddInstanceCommandTests.cs | 93 +++++++++++++++++++ .../Queries/GetInstanceByIdQueryTests.cs | 75 +++++++++++++++ .../EdFi.Ods.AdminConsole.DBTests.csproj | 32 +++++++ .../PlatformUsersContextTestBase.cs | 83 +++++++++++++++++ .../EdFi.Ods.AdminConsole.DBTests/Testing.cs | 51 ++++++++++ .../appsettings.json | 17 ++++ 34 files changed, 1226 insertions(+), 12 deletions(-) create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/AddInstance.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/InstanceModel.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/ReadInstances.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/Encryption.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeyResolver.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeySettings.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/OptionsEncryptionKeyResolver.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.Designer.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/ModelConfiguration/InstanceConfiguration.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/IModel.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/Instance.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/EncryptionService.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Commands/AddInstanceCommand.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstanceQuery.cs create mode 100644 Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstancesQuery.cs create mode 100644 Application/EdFi.Ods.AdminApi/Helpers/AdminConsoleSettings.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/AssertionExtensions.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/Database/CommandTests/AddInstanceCommandTests.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/Database/Queries/GetInstanceByIdQueryTests.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/EdFi.Ods.AdminConsole.DBTests.csproj create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/PlatformUsersContextTestBase.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs create mode 100644 Application/EdFi.Ods.AdminConsole.DBTests/appsettings.json diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index 9e7aac5c9..ed874cb99 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -19,7 +19,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "IntegrationTests", "Integra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.DBTests", "EdFi.Ods.AdminApi.DBTests\EdFi.Ods.AdminApi.DBTests.csproj", "{73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.AdminConsole", "EdFi.Ods.AdminApi.AdminConsole\EdFi.Ods.AdminApi.AdminConsole.csproj", "{0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminApi.AdminConsole", "EdFi.Ods.AdminApi.AdminConsole\EdFi.Ods.AdminApi.AdminConsole.csproj", "{0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdFi.Ods.AdminConsole.DBTests", "EdFi.Ods.AdminConsole.DBTests\EdFi.Ods.AdminConsole.DBTests.csproj", "{A2DC17AC-66C2-4119-BB47-4266E8ACB055}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -61,6 +63,14 @@ Global {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Release|Any CPU.Build.0 = Release|Any CPU {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Release|x64.ActiveCfg = Release|Any CPU {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Release|x64.Build.0 = Release|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Debug|x64.ActiveCfg = Debug|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Debug|x64.Build.0 = Debug|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Release|Any CPU.Build.0 = Release|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Release|x64.ActiveCfg = Release|Any CPU + {A2DC17AC-66C2-4119-BB47-4266E8ACB055}.Release|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -68,6 +78,7 @@ Global GlobalSection(NestedProjects) = preSolution {F62C9CF6-A632-4894-B61A-674198DAB86E} = {9A9D18B4-718D-4681-BAFE-A1C42E18A7CC} {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} + {A2DC17AC-66C2-4119-BB47-4266E8ACB055} = {D8A26B59-6DAD-4046-9DDE-00D2CFDAE9B6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {30CF2BE4-58CA-4598-9B59-D334FC971A0F} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj b/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj index fcfa50dc6..f966405d1 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -24,6 +24,7 @@ + diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/AddInstance.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/AddInstance.cs new file mode 100644 index 000000000..8356ee849 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/AddInstance.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.ComponentModel.DataAnnotations; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Commands; +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace EdFi.Ods.AdminApi.AdminConsole.Features.Instances; + +public class AddInstance : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiAdminConsoleEndpointBuilder.MapPost(endpoints, "/instances", Execute) + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(); + } + + public async Task Execute(Validator validator, IAddInstanceCommand addInstanceCommand, AddInstanceRequest request) + { + await validator.GuardAsync(request); + var addedInstanceResult = await addInstanceCommand.Execute(request); + + return Results.Created($"/instances/{addedInstanceResult.DocId}", addedInstanceResult); + } + + public class AddInstanceRequest : IAddInstanceModel + { + [Required] + public int InstanceId { get; set; } + [Required] + public int EdOrgId { get; set; } + [Required] + public int TenantId { get; set; } + [Required] + public string Document { get; set; } + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(m => m.InstanceId) + .NotNull(); + + RuleFor(m => m.EdOrgId) + .NotNull(); + + RuleFor(m => m.Document) + .NotNull() + .NotEmpty() + .Must(BeValidDocument).WithMessage("Document must be a valid JSON."); + } + + private bool BeValidDocument(string document) + { + try + { + Newtonsoft.Json.Linq.JToken.Parse(document); + return true; + } + catch (Newtonsoft.Json.JsonReaderException) + { + return false; + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/InstanceModel.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/InstanceModel.cs new file mode 100644 index 000000000..68eff69b2 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/InstanceModel.cs @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Newtonsoft.Json; + +namespace EdFi.Ods.AdminApi.AdminConsole.Features.OdsInstances; + +public class InstanceModel +{ + [JsonProperty("DocId")] + public int DocId { get; set; } + + [JsonProperty("InstanceId")] + public int? InstanceId { get; set; } + + [JsonProperty("TenantId")] + public int? TenantId { get; set; } + + [JsonProperty("EdOrgId")] + public int? EdOrgId { get; set; } + + [JsonProperty("Document")] + public string? Document { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/ReadInstances.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/ReadInstances.cs new file mode 100644 index 000000000..7b7fc9e79 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Instances/ReadInstances.cs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using AutoMapper; +using EdFi.Ods.AdminApi.AdminConsole.Features.OdsInstances; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace EdFi.Ods.AdminApi.AdminConsole.Features.UserProfiles; + +public class ReadInstances : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiAdminConsoleEndpointBuilder.MapGet(endpoints, "/instances", GetInstances) + .BuildForVersions(); + + AdminApiAdminConsoleEndpointBuilder.MapGet(endpoints, "/instances/{id}", GetInstance) + .WithRouteOptions(b => b.WithResponse(200)) + .BuildForVersions(); + } + + internal async Task GetInstances([FromServices] IGetInstancesQuery getInstancesQuery) + { + var instances = await getInstancesQuery.Execute(); + return Results.Ok(instances); + } + internal async Task GetInstance([FromServices] IGetInstanceQuery getInstanceQuery, int docId) + { + var instance = await getInstanceQuery.Execute(docId); + return Results.Ok(instance); + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/Encryption.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/Encryption.cs new file mode 100644 index 000000000..4bc1c79be --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/Encryption.cs @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Rijndael256; + +namespace EdFi.Ods.AdminApi.AdminConsole.Helpers; +public class Encryption : RijndaelEtM +{ + /// + /// Encrypts plaintext using the Encrypt-then-MAC (EtM) mode via the Rijndael cipher in + /// CBC mode with a password derived HMAC SHA-512 salt. A random 128-bit Initialization + /// Vector is generated for the cipher. + /// + /// The plainText to encrypt. + /// The encryptionKey to encrypt the plainText with. + /// The Base64 encoded EtM ciphertext. + public static string Encrypt(string plainText, string encryptionKey) + { + return Encrypt(plainText, encryptionKey, KeySize.Aes256); + } + + /// + /// Decrypts EtM ciphertext using the Rijndael cipher in CBC mode with a password derived + /// HMAC SHA-512 salt. + /// + /// The Base64 encoded EtM ciphertext to decrypt. + /// The encryptionKey to decrypt the EtM ciphertext with. + /// The plaintext. + public static string Decrypt(string ciphertText, string encryptionKey) + { + return Decrypt(ciphertText, encryptionKey, KeySize.Aes256); + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeyResolver.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeyResolver.cs new file mode 100644 index 000000000..f4c806440 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeyResolver.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.AdminConsole.Helpers; + +public interface IEncryptionKeyResolver +{ + string GetEncryptionKey(); +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeySettings.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeySettings.cs new file mode 100644 index 000000000..3e3ffcda4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/IEncryptionKeySettings.cs @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.AdminConsole.Helpers; + +public interface IEncryptionKeySettings +{ + public string EncryptionKey { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/OptionsEncryptionKeyResolver.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/OptionsEncryptionKeyResolver.cs new file mode 100644 index 000000000..306b4b100 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Helpers/OptionsEncryptionKeyResolver.cs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.AdminConsole.Helpers; + +public class OptionsEncryptionKeyResolver : IEncryptionKeyResolver +{ + private readonly string _encryptionKey; + + public OptionsEncryptionKeyResolver(IEncryptionKeySettings encryptionKeySettings) + { + _encryptionKey = encryptionKeySettings.EncryptionKey; + } + + public string GetEncryptionKey() + { + return _encryptionKey; + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030014056_InitialCreate.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030014056_InitialCreate.cs index 62b0f0053..0e0fd7149 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030014056_InitialCreate.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030014056_InitialCreate.cs @@ -1,4 +1,9 @@ -using Microsoft.EntityFrameworkCore.Migrations; +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.Designer.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.Designer.cs new file mode 100644 index 000000000..d6fd5b6ce --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.Designer.cs @@ -0,0 +1,88 @@ +// +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Contexts.AdminConsoleSql; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Artifacts.MsSql +{ + [DbContext(typeof(AdminConsoleSqlContext))] + [Migration("20241030141215_InstanceTables")] + partial class InstanceTables + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models.HealthCheck", b => + { + b.Property("DocId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("DocId")); + + b.Property("Document") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EdOrgId") + .HasColumnType("int"); + + b.Property("InstanceId") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("int"); + + b.HasKey("DocId"); + + b.HasIndex("EdOrgId"); + + b.HasIndex("InstanceId"); + + b.HasIndex("TenantId") + .IsUnique(); + + b.ToTable("HealthChecks", "adminconsole"); + }); + + modelBuilder.Entity("EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models.Instance", b => + { + b.Property("InstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InstanceId")); + + b.Property("DocId") + .HasColumnType("int"); + + b.Property("Document") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EdOrgId") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("int"); + + b.HasKey("InstanceId"); + + b.ToTable("Instances"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.cs new file mode 100644 index 000000000..6bca642d8 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/20241030141215_InstanceTables.cs @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Artifacts.MsSql +{ + /// + public partial class InstanceTables : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.EnsureSchema( + name: "adminconsole"); + + migrationBuilder.CreateTable( + name: "Instances", + schema: "adminconsole", + columns: table => new + { + DocId = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + InstanceId = table.Column(type: "int", nullable: false), + EdOrgId = table.Column(type: "int", nullable: false), + TenantId = table.Column(type: "int", nullable: false), + Document = table.Column(type: "nvarchar(max)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Instances", x => x.DocId); + }); + + migrationBuilder.CreateIndex( + name: "IX_Instances_EdOrgId", + schema: "adminconsole", + table: "Instances", + column: "EdOrgId"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_InstanceId", + schema: "adminconsole", + table: "Instances", + column: "InstanceId"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_TenantId", + schema: "adminconsole", + table: "Instances", + column: "TenantId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Instances", + schema: "adminconsole"); + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/AdminConsoleSqlContextModelSnapshot.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/AdminConsoleSqlContextModelSnapshot.cs index 0389c9529..7d75afec3 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/AdminConsoleSqlContextModelSnapshot.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Artifacts/MsSql/AdminConsoleSqlContextModelSnapshot.cs @@ -53,6 +53,32 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("HealthChecks", "adminconsole"); }); + + modelBuilder.Entity("EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models.Instance", b => + { + b.Property("InstanceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("InstanceId")); + + b.Property("DocId") + .HasColumnType("int"); + + b.Property("Document") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("EdOrgId") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("int"); + + b.HasKey("InstanceId"); + + b.ToTable("Instances"); + }); #pragma warning restore 612, 618 } } diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsolePg/AdminConsolePgContext.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsolePg/AdminConsolePgContext.cs index d4f4373df..3b4e1c4f4 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsolePg/AdminConsolePgContext.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsolePg/AdminConsolePgContext.cs @@ -15,9 +15,12 @@ public AdminConsolePgContext(DbContextOptions options) : public DbSet HealthChecks { get; set; } + public DbSet Instances { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { const string DbProvider = DbProviders.PostgreSql; modelBuilder.ApplyConfiguration(new HealthCheckConfiguration(DbProvider)); + modelBuilder.ApplyConfiguration(new InstanceConfiguration(DbProvider)); } } diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsoleSql/AdminConsoleSqlContext.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsoleSql/AdminConsoleSqlContext.cs index 6f0eac9ab..3b23f2625 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsoleSql/AdminConsoleSqlContext.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/AdminConsoleSql/AdminConsoleSqlContext.cs @@ -15,9 +15,12 @@ public AdminConsoleSqlContext(DbContextOptions options) public DbSet HealthChecks { get; set; } + public DbSet Instances { get; set; } + protected override void OnModelCreating(ModelBuilder modelBuilder) { const string DbProvider = DbProviders.SqlServer; modelBuilder.ApplyConfiguration(new HealthCheckConfiguration(DbProvider)); + modelBuilder.ApplyConfiguration(new InstanceConfiguration(DbProvider)); } } diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/IDbContext.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/IDbContext.cs index d424bf9a3..5c6d0833f 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/IDbContext.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Contexts/IDbContext.cs @@ -3,14 +3,8 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore; -using System.Xml; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using Microsoft.EntityFrameworkCore; namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Contexts; @@ -18,6 +12,8 @@ public interface IDbContext { DbSet HealthChecks { get; set; } + DbSet Instances { get; set; } + DbSet Set() where T : class; Task SaveChangesAsync(CancellationToken cancellationToken = default); } diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/ModelConfiguration/InstanceConfiguration.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/ModelConfiguration/InstanceConfiguration.cs new file mode 100644 index 000000000..83fa2be7d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/ModelConfiguration/InstanceConfiguration.cs @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.ModelConfiguration; + +public class InstanceConfiguration : IEntityTypeConfiguration +{ + private readonly string _dbProvider; + public InstanceConfiguration(string dbProvider) + { + _dbProvider = dbProvider; + } + + public void Configure(EntityTypeBuilder entity) + { + entity.ToTable("Instances", "adminconsole"); + entity.HasKey(e => e.DocId); + switch (_dbProvider) + { + case DbProviders.PostgreSql: + entity.Property(e => e.Document).HasColumnType("jsonb"); + break; + case DbProviders.SqlServer: + entity.Property(e => e.Document).HasColumnType("nvarchar(max)"); + break; + } + + entity.HasIndex(e => e.InstanceId); + entity.HasIndex(e => e.EdOrgId); + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/IModel.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/IModel.cs new file mode 100644 index 000000000..2c8275f6f --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/IModel.cs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models +{ + public interface IModel + { + int? DocId { get; set; } + int InstanceId { get; set; } + int TenantId { get; set; } + int EdOrgId { get; set; } + string Document { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/Instance.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/Instance.cs new file mode 100644 index 000000000..ca730efe4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/DataAccess/Models/Instance.cs @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; + +public class Instance : IModel +{ + public int? DocId { get; set; } + public required int InstanceId { get; set; } + public required int TenantId { get; set; } + public required int EdOrgId { get; set; } + public required string Document { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/EncryptionService.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/EncryptionService.cs new file mode 100644 index 000000000..294dbc73c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/EncryptionService.cs @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using Microsoft.Extensions.Logging; +using static EdFi.Ods.AdminApi.AdminConsole.Helpers.Encryption; + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; + +public interface IEncryptionService +{ + bool TryEncrypt(string plainText, string encryptionKey, out string encryptedText); + bool TryDecrypt(string encryptedText, string encryptionKey, out string decryptedText); +} + +public class EncryptionService : IEncryptionService +{ + //private readonly ILogger _logger; + + public EncryptionService(/*ILogger logger*/) + { + //_logger = logger; + } + + public bool TryEncrypt(string plainText, string encryptionKey, out string encryptedText) + { + encryptedText = string.Empty; + if (string.IsNullOrEmpty(encryptionKey)) + { + //_logger.LogError("Encryption key can not be empty"); + return false; + } + + try + { + encryptedText = Encrypt(plainText, encryptionKey); + return true; + } + catch (Exception ex) + { + //_logger.LogError(ex, "Provided encryption key is not valid."); + } + + return false; + } + + public bool TryDecrypt(string encryptedText, string encryptionKey, out string decryptedText) + { + decryptedText = string.Empty; + if (string.IsNullOrEmpty(encryptionKey)) + { + //_logger.LogError("Encryption key can not be empty"); + return false; + } + + try + { + decryptedText = Decrypt(encryptedText, encryptionKey); + return true; + } + catch (Exception ex) + { + //_logger.LogError(ex, "Provided encryption key is not valid."); + } + + return false; + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Commands/AddInstanceCommand.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Commands/AddInstanceCommand.cs new file mode 100644 index 000000000..9ab6bb54c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Commands/AddInstanceCommand.cs @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.Json.Nodes; +using EdFi.Ods.AdminApi.AdminConsole.Helpers; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Commands; + +public interface IAddInstanceCommand +{ + Task Execute(IAddInstanceModel instance); +} + +public class AddInstanceCommand : IAddInstanceCommand +{ + private readonly ICommandRepository _instanceCommand; + private readonly IEncryptionService _encryptionService; + private readonly string _encryptionKey; + + public AddInstanceCommand(ICommandRepository instanceCommand, IEncryptionKeyResolver encryptionKeyResolver, IEncryptionService encryptionService) + { + _instanceCommand = instanceCommand; + _encryptionKey = encryptionKeyResolver.GetEncryptionKey(); + _encryptionService = encryptionService; + } + + public async Task Execute(IAddInstanceModel instance) + { + JsonNode? jnDocument = JsonNode.Parse(instance.Document); + + var clientId = jnDocument!["clientId"]?.AsValue().ToString(); + var clientSecret = jnDocument!["clientSecret"]?.AsValue().ToString(); + + var encryptedClientId = string.Empty; + var encryptedClientSecret = string.Empty; + + if (!string.IsNullOrEmpty(clientId) && !string.IsNullOrEmpty(clientSecret)) + { + _encryptionService.TryEncrypt(clientId, _encryptionKey, out encryptedClientId); + _encryptionService.TryEncrypt(clientSecret, _encryptionKey, out encryptedClientSecret); + + jnDocument!["clientId"] = encryptedClientId; + jnDocument!["clientSecret"] = encryptedClientSecret; + } + + try + { + return await _instanceCommand.AddAsync(new Instance + { + InstanceId = instance.InstanceId, + TenantId = instance.TenantId, + EdOrgId = instance.EdOrgId, + Document = jnDocument!.ToJsonString(), + }); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + return null; + } + } +} + +public interface IAddInstanceModel +{ + int InstanceId { get; } + int EdOrgId { get; } + int TenantId { get; } + string Document { get; } +} + +public class AddInstanceResult +{ + public int DocId { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstanceQuery.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstanceQuery.cs new file mode 100644 index 000000000..203b1e6d0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstanceQuery.cs @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.Json.Nodes; +using EdFi.Ods.AdminApi.AdminConsole.Helpers; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; +using Microsoft.EntityFrameworkCore; + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; + +public interface IGetInstanceQuery +{ + Task Execute(int docId); +} + +public class GetInstanceQuery : IGetInstanceQuery +{ + private readonly IQueriesRepository _instanceQuery; + private readonly IEncryptionService _encryptionService; + private readonly string _encryptionKey; + + public GetInstanceQuery(IQueriesRepository instanceQuery, IEncryptionKeyResolver encryptionKeyResolver, IEncryptionService encryptionService) + { + _instanceQuery = instanceQuery; + _encryptionKey = encryptionKeyResolver.GetEncryptionKey(); + _encryptionService = encryptionService; + } + public async Task Execute(int docId) + { + + var instance = await _instanceQuery.Query().SingleOrDefaultAsync(instance => instance.DocId == docId); + + if (instance == null) + { + throw new Exception($"Not found {nameof(Instance)} for Doc Id: {docId}"); + } + + JsonNode? jnDocument = JsonNode.Parse(instance.Document); + + var encryptedClientId = jnDocument!["clientId"]?.AsValue().ToString(); + var encryptedClientSecret = jnDocument!["clientSecret"]?.AsValue().ToString(); + + var clientId = string.Empty; + var clientSecret = string.Empty; + + if (!string.IsNullOrEmpty(encryptedClientId) && !string.IsNullOrEmpty(encryptedClientSecret)) + { + _encryptionService.TryDecrypt(encryptedClientId, _encryptionKey, out clientId); + _encryptionService.TryDecrypt(encryptedClientSecret, _encryptionKey, out clientSecret); + + jnDocument!["clientId"] = clientId; + jnDocument!["clientSecret"] = clientSecret; + } + + instance.Document = jnDocument!.ToJsonString(); + + return instance; + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstancesQuery.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstancesQuery.cs new file mode 100644 index 000000000..85ca98197 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Instances/Queries/GetInstancesQuery.cs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Text.Json.Nodes; +using EdFi.Ods.AdminApi.AdminConsole.Helpers; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; + +namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; + +public interface IGetInstancesQuery +{ + Task> Execute(); +} + +public class GetInstancesQuery : IGetInstancesQuery +{ + private readonly IQueriesRepository _instanceQuery; + private readonly IEncryptionService _encryptionService; + private readonly string _encryptionKey; + + public GetInstancesQuery(IQueriesRepository instanceQuery, IEncryptionKeyResolver encryptionKeyResolver, IEncryptionService encryptionService) + { + _instanceQuery = instanceQuery; + _encryptionKey = encryptionKeyResolver.GetEncryptionKey(); + _encryptionService = encryptionService; + } + public async Task> Execute() + { + var instances = await _instanceQuery.GetAllAsync(); + + foreach (var instance in instances) + { + JsonNode? jn = JsonNode.Parse(instance.Document); + + var encryptedClientId = jn!["clientId"]?.AsValue().ToString(); + var encryptedClientSecret = jn!["clientSecret"]?.AsValue().ToString(); + + var clientId = string.Empty; + var clientSecret = string.Empty; + + if (!string.IsNullOrEmpty(encryptedClientId) && !string.IsNullOrEmpty(encryptedClientSecret)) + { + _encryptionService.TryDecrypt(encryptedClientId, _encryptionKey, out clientId); + _encryptionService.TryDecrypt(encryptedClientSecret, _encryptionKey, out clientSecret); + + jn!["clientId"] = clientId; + jn!["clientSecret"] = clientSecret; + } + + instance.Document = jn!.ToJsonString(); + } + + return instances.ToList(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/ServiceRegistration.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/ServiceRegistration.cs index 6cdc8718e..86adfb491 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/ServiceRegistration.cs +++ b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/ServiceRegistration.cs @@ -4,11 +4,16 @@ // See the LICENSE and NOTICES files in the project root for more information. using EdFi.Ods.AdminApi.AdminConsole.Features.Healthcheck; +using EdFi.Ods.AdminApi.AdminConsole.Features.Instances; +using EdFi.Ods.AdminApi.AdminConsole.Helpers; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Commands; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.HealthChecks.Queries; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Commands; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; @@ -20,6 +25,11 @@ public static void AddRepositories(this IServiceCollection serviceCollection) serviceCollection.AddScoped, CommandRepository>(); serviceCollection.AddScoped, QueriesRepository>(); #endregion + + #region Instance + serviceCollection.AddScoped, CommandRepository>(); + serviceCollection.AddScoped, QueriesRepository>(); + #endregion } public static void AddServices(this IServiceCollection serviceCollection) @@ -29,10 +39,17 @@ public static void AddServices(this IServiceCollection serviceCollection) serviceCollection.AddScoped(); serviceCollection.AddScoped(); #endregion Healthcheck + + #region Instance + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + serviceCollection.AddScoped(); + #endregion Instance } public static void AddValidators(this IServiceCollection serviceCollection) { serviceCollection.AddTransient(); + serviceCollection.AddTransient(); } } diff --git a/Application/EdFi.Ods.AdminApi/Helpers/AdminConsoleSettings.cs b/Application/EdFi.Ods.AdminApi/Helpers/AdminConsoleSettings.cs new file mode 100644 index 000000000..5653baf98 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Helpers/AdminConsoleSettings.cs @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.AdminConsole.Helpers; + +namespace EdFi.Ods.AdminApi.Helpers; + +public class AdminConsoleSettings : IEncryptionKeySettings +{ + public string EncryptionKey { get; set; } = string.Empty; +} diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 4fc0283ec..64eca0825 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -4,14 +4,21 @@ // The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. // See the LICENSE and NOTICES files in the project root for more information. +using System.Configuration; using AspNetCoreRateLimit; +using Autofac.Core; +using EdFi.Ods.AdminApi.AdminConsole.Helpers; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.AutoMapper; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess; using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; using EdFi.Ods.AdminApi.Features; +using EdFi.Ods.AdminApi.Helpers; using EdFi.Ods.AdminApi.Infrastructure; using EdFi.Ods.AdminApi.Infrastructure.MultiTenancy; using log4net; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using ServiceRegistration = EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.ServiceRegistration; var builder = WebApplication.CreateBuilder(args); @@ -23,6 +30,12 @@ builder.Services.AddSingleton(); builder.Services.AddInMemoryRateLimiting(); +builder.Services.Configure(builder.Configuration.GetSection("AdminConsoleSettings")); +#pragma warning disable CS8602 // Dereference of a possibly null reference. +builder.Services.AddTransient(sp => sp.GetService>().Value); +#pragma warning restore CS8602 // Dereference of a possibly null reference. +builder.Services.AddTransient(); +builder.Services.AddScoped(); ServiceRegistration.AddRepositories(builder.Services); ServiceRegistration.AddServices(builder.Services); diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 76201b599..5a2afbdc3 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -25,10 +25,13 @@ "EnableSwagger": false, "DefaultTenant": "" }, + "AdminConsoleSettings": { + "EncryptionKey": "abcdefghi" + }, "EnableDockerEnvironment": false, "ConnectionStrings": { - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True", - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True" + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True;Encrypt=True;TrustServerCertificate=True", + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True;Encrypt=True;TrustServerCertificate=True" }, "Log4NetCore": { "Log4NetConfigFileName": "log4net\\log4net.config" diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/AssertionExtensions.cs b/Application/EdFi.Ods.AdminConsole.DBTests/AssertionExtensions.cs new file mode 100644 index 000000000..515cc6f86 --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/AssertionExtensions.cs @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentValidation; +using FluentValidation.Results; +using Shouldly; +using static System.Environment; + +namespace EdFi.Ods.AdminConsole.DBTests; + +public static class AssertionExtensions +{ + public static void ShouldValidate(this AbstractValidator validator, TModel model) + => validator.Validate(model).ShouldBeSuccessful(); + + public static void ShouldNotValidate(this AbstractValidator validator, TModel model, params string[] expectedErrors) + => validator.Validate(model).ShouldBeFailure(expectedErrors); + + private static void ShouldBeSuccessful(this ValidationResult result) + { + var indentedErrorMessages = result + .Errors + .OrderBy(x => x.ErrorMessage) + .Select(x => " " + x.ErrorMessage) + .ToArray(); + + var actual = string.Join(NewLine, indentedErrorMessages); + + result.IsValid.ShouldBeTrue($"Expected no validation errors, but found {result.Errors.Count}:{NewLine}{actual}"); + } + + private static void ShouldBeFailure(this ValidationResult result, params string[] expectedErrors) + { + result.IsValid.ShouldBeFalse("Expected validation errors, but the message passed validation."); + + result.Errors + .OrderBy(x => x.ErrorMessage) + .Select(x => x.ErrorMessage) + .ToArray() + .ShouldBe(expectedErrors.OrderBy(x => x).ToArray()); + } + + public static void ShouldSatisfy(this IEnumerable actual, params Action[] itemExpectations) + { + var actualItems = actual.ToArray(); + + if (actualItems.Length != itemExpectations.Length) + throw new Exception( + $"Expected the collection to have {itemExpectations.Length} " + + $"items, but there were {actualItems.Length} items."); + + for (var i = 0; i < actualItems.Length; i++) + { + try + { + itemExpectations[i](actualItems[i]); + } + catch (Exception failure) + { + throw new Exception($"Assertion failed for item at position [{i}].", failure); + } + } + } +} diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/Database/CommandTests/AddInstanceCommandTests.cs b/Application/EdFi.Ods.AdminConsole.DBTests/Database/CommandTests/AddInstanceCommandTests.cs new file mode 100644 index 000000000..a7d42c51b --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/Database/CommandTests/AddInstanceCommandTests.cs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Commands; +using EdFi.Ods.AdminApi.Helpers; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminConsole.DBTests.Database.CommandTests; + +[TestFixture] +public class AddInstanceCommandTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + + [OneTimeSetUp] + public virtual async Task FixtureSetup() + { + AdminConsoleSettings appSettings = new AdminConsoleSettings(); + await Task.Yield(); + } + + [Test] + public void ShouldExecute() + { + var instanceDocument = "{\"instanceId\":\"DEF456\",\"tenantId\":\"def456\",\"instanceName\":\"Mock Instance 2\",\"instanceType\":\"Type B\",\"connectionType\":\"Type Y\",\"clientId\":\"CLIENT321\",\"clientSecret\":\"SECRET456\",\"baseUrl\":\"https://localhost/api\",\"authenticationUrl\":\"https://localhost/api/oauth/token\",\"resourcesUrl\":\"https://localhost/api\",\"schoolYears\":[2024,2025],\"isDefault\":false,\"verificationStatus\":null,\"provider\":\"Local\"}"; + + var encryptionService = new EncryptionService(); + var encryptionKey = Testing.GetEncryptionKeyResolver().GetEncryptionKey(); + + Transaction(async dbContext => + { + var repository = new CommandRepository(dbContext); + var newInstance = new TestInstance + { + InstanceId = 1, + TenantId = 1, + EdOrgId = 1, + Document = instanceDocument + }; + + var command = new AddInstanceCommand(repository, Testing.GetEncryptionKeyResolver(), encryptionService); + + var result = await command.Execute(newInstance); + }); + + Transaction(dbContext => + { + var persistedInstance = dbContext.Instances; + persistedInstance.Count().ShouldBe(1); + persistedInstance.First().DocId.ShouldBe(1); + persistedInstance.First().TenantId.ShouldBe(1); + persistedInstance.First().InstanceId.ShouldBe(1); + persistedInstance.First().EdOrgId.ShouldBe(1); + + JsonNode jnDocument = JsonNode.Parse(persistedInstance.First().Document); + + var encryptedClientId = jnDocument!["clientId"]?.AsValue().ToString(); + var encryptedClientSecret = jnDocument!["clientSecret"]?.AsValue().ToString(); + + var clientId = "CLIENT321"; + var clientSecret = "SECRET456"; + + if (!string.IsNullOrEmpty(encryptedClientId) && !string.IsNullOrEmpty(encryptedClientSecret)) + { + encryptionService.TryDecrypt(encryptedClientId, encryptionKey, out clientId); + encryptionService.TryDecrypt(encryptedClientSecret, encryptionKey, out clientSecret); + + clientId.ShouldBe(clientId); + clientSecret.ShouldBe(clientSecret); + } + }); + } + + private class TestInstance : IAddInstanceModel + { + public int DocId { get; } + public int TenantId { get; set; } + public int InstanceId { get; set; } + public int EdOrgId { get; set; } + public string Document { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/Database/Queries/GetInstanceByIdQueryTests.cs b/Application/EdFi.Ods.AdminConsole.DBTests/Database/Queries/GetInstanceByIdQueryTests.cs new file mode 100644 index 000000000..0d88b2f70 --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/Database/Queries/GetInstanceByIdQueryTests.cs @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Commands; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Instances.Queries; +using EdFi.Ods.AdminApi.Helpers; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminConsole.DBTests.Database.CommandTests; + +[TestFixture] +public class GetInstanceByIdQueryTests : PlatformUsersContextTestBase +{ + private IOptions _options { get; set; } + + [OneTimeSetUp] + public virtual async Task FixtureSetup() + { + AdminConsoleSettings appSettings = new AdminConsoleSettings(); + await Task.Yield(); + } + + [Test] + public void ShouldExecute() + { + var instanceDocument = "{\"instanceId\":\"DEF456\",\"tenantId\":\"def456\",\"instanceName\":\"Mock Instance 2\",\"instanceType\":\"Type B\",\"connectionType\":\"Type Y\",\"clientId\":\"CLIENT321\",\"clientSecret\":\"SECRET456\",\"baseUrl\":\"https://localhost/api\",\"authenticationUrl\":\"https://localhost/api/oauth/token\",\"resourcesUrl\":\"https://localhost/api\",\"schoolYears\":[2024,2025],\"isDefault\":false,\"verificationStatus\":null,\"provider\":\"Local\"}"; + Instance result = null; + + var newInstance = new TestInstance + { + InstanceId = 1, + TenantId = 1, + EdOrgId = 1, + Document = instanceDocument + }; + + Transaction(async dbContext => + { + var repository = new CommandRepository(dbContext); + var command = new AddInstanceCommand(repository, Testing.GetEncryptionKeyResolver(), new EncryptionService()); + + result = await command.Execute(newInstance); + }); + + Transaction(async dbContext => + { + var repository = new QueriesRepository(dbContext); + var query = new GetInstanceQuery(repository, Testing.GetEncryptionKeyResolver(), new EncryptionService()); + var instance = await query.Execute(result.DocId.Value); + + instance.DocId.ShouldBe(result.DocId); + instance.TenantId.ShouldBe(newInstance.TenantId); + instance.InstanceId.ShouldBe(newInstance.InstanceId); + instance.EdOrgId.ShouldBe(newInstance.EdOrgId); + instance.Document.ShouldBe(newInstance.Document); + }); + } + + private class TestInstance : IAddInstanceModel + { + public int DocId { get; } + public int TenantId { get; set; } + public int InstanceId { get; set; } + public int EdOrgId { get; set; } + public string Document { get; set; } + } +} diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/EdFi.Ods.AdminConsole.DBTests.csproj b/Application/EdFi.Ods.AdminConsole.DBTests/EdFi.Ods.AdminConsole.DBTests.csproj new file mode 100644 index 000000000..863000e8f --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/EdFi.Ods.AdminConsole.DBTests.csproj @@ -0,0 +1,32 @@ + + + net8.0 + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/PlatformUsersContextTestBase.cs b/Application/EdFi.Ods.AdminConsole.DBTests/PlatformUsersContextTestBase.cs new file mode 100644 index 000000000..3c6b202cd --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/PlatformUsersContextTestBase.cs @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using System; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Contexts; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Contexts.AdminConsoleSql; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.DataAccess.Models; +using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Repository; +using Microsoft.EntityFrameworkCore; +using NUnit.Framework; +using Respawn; +using static EdFi.Ods.AdminConsole.DBTests.Testing; + +namespace EdFi.Ods.AdminConsole.DBTests; + +[TestFixture] +public abstract class PlatformUsersContextTestBase +{ + private readonly Checkpoint _checkpoint = new() + { + TablesToIgnore = new[] + { + "__MigrationHistory", "DeployJournal", "AdminConsoleDeployJournal" + }, + SchemasToExclude = Array.Empty() + }; + + protected static string ConnectionString => AdminConnectionString; + + [OneTimeTearDown] + public async Task FixtureTearDown() + { + await _checkpoint.Reset(ConnectionString); + } + + [SetUp] + public async Task SetUp() + { + await _checkpoint.Reset(ConnectionString); + } + + protected static void Save(params object[] entities) + { + Transaction(dbContext => + { + foreach (var entity in entities) + { + ((AdminConsoleSqlContext)dbContext).Add(entity); + } + }); + } + + protected static async void Transaction(Action action) + { + using var dbContext = new AdminConsoleSqlContext(GetDbContextOptions()); + using var transaction = (dbContext).Database.BeginTransaction(); + action(dbContext); + dbContext.SaveChanges(); + transaction.Commit(); + } + + protected static TResult Transaction(Func query) + { + var result = default(TResult); + + Transaction(database => + { + result = query(database); + }); + + return result; + } + + protected static DbContextOptions GetDbContextOptions() + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer(ConnectionString); + return builder.Options; + } +} diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs b/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs new file mode 100644 index 000000000..9cf7e460a --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/Testing.cs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Licensed to the Ed-Fi Alliance under one or more agreements. +// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0. +// See the LICENSE and NOTICES files in the project root for more information. + +using EdFi.Ods.AdminApi.AdminConsole.Helpers; +using EdFi.Ods.AdminApi.Helpers; +using FakeItEasy; +using Microsoft.AspNetCore.Builder; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminConsole.DBTests; + +public static class Testing +{ + private static IConfigurationRoot _config; + + public static IConfiguration Configuration() + { + _config ??= new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + return _config; + } + + public static string AdminConnectionString { get { return Configuration().GetConnectionString("EdFi_Admin"); } } + + public static DbContextOptions GetDbContextOptions(string connectionString) + { + var builder = new DbContextOptionsBuilder(); + builder.UseSqlServer(connectionString); + return builder.Options; + } + + public static IOptions GetAppSettings() + { + AppSettings appSettings = new AppSettings(); + IOptions options = Options.Create(appSettings); + return options; + } + + public static IEncryptionKeyResolver GetEncryptionKeyResolver() + { + var encryptionKeyResolver = A.Fake(); + A.CallTo(() => encryptionKeyResolver.GetEncryptionKey()).Returns("lskdjflskdjf"); + return encryptionKeyResolver; + } +} diff --git a/Application/EdFi.Ods.AdminConsole.DBTests/appsettings.json b/Application/EdFi.Ods.AdminConsole.DBTests/appsettings.json new file mode 100644 index 000000000..4483f8981 --- /dev/null +++ b/Application/EdFi.Ods.AdminConsole.DBTests/appsettings.json @@ -0,0 +1,17 @@ +{ + "AppSettings": { + "AppStartup": "OnPrem", + "ApiStartupType": "sandbox", + "XsdFolder": "Schema", + "DatabaseEngine": "SqlServer", + "DefaultPageSizeOffset": 0, + "DefaultPageSizeLimit": 25 + }, + "ConnectionStrings": { + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Test;Integrated Security=True;Encrypt=false;Trusted_Connection=true", + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Test;Integrated Security=True;Encrypt=false;Trusted_Connection=true" + }, + "AdminConsoleSettings": { + "EncryptionKey": "abcdefghi" + } +}