diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/UserMapping.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/UserMapping.cs index 3296cfff0..1fb44c3c5 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/UserMapping.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Mappings/UserMapping.cs @@ -35,6 +35,8 @@ public class ApplicationUserMapping : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.Property(e => e.ApiRoles).HasColumnType("varchar[]"); + builder.Property(e => e.OneLoginClientId).HasMaxLength(50); + builder.Property(e => e.OneLoginPrivateKeyPem).HasMaxLength(2000); } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.Designer.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.Designer.cs new file mode 100644 index 000000000..76ca0fe7a --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.Designer.cs @@ -0,0 +1,1033 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TeachingRecordSystem.Core.DataStore.Postgres; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + [DbContext(typeof(TrsDbContext))] + [Migration("20240129115405_ApplicationUserOneLoginClientIdAndPem")] + partial class ApplicationUserOneLoginClientIdAndPem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.Property("ApiKeyId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("api_key_id"); + + b.Property("ApplicationUserId") + .HasColumnType("uuid") + .HasColumnName("application_user_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("key"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("ApiKeyId") + .HasName("pk_api_keys"); + + b.HasIndex("ApplicationUserId") + .HasDatabaseName("ix_api_keys_application_user_id"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_api_keys_key"); + + b.ToTable("api_keys", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EntityChangesJournal", b => + { + b.Property("Key") + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("EntityLogicalName") + .HasColumnType("text") + .HasColumnName("entity_logical_name"); + + b.Property("DataToken") + .HasColumnType("text") + .HasColumnName("data_token"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_updated"); + + b.Property("LastUpdatedBy") + .HasColumnType("text") + .HasColumnName("last_updated_by"); + + b.Property("NextQueryPageNumber") + .HasColumnType("integer") + .HasColumnName("next_query_page_number"); + + b.Property("NextQueryPageSize") + .HasColumnType("integer") + .HasColumnName("next_query_page_size"); + + b.Property("NextQueryPagingCookie") + .HasColumnType("text") + .HasColumnName("next_query_paging_cookie"); + + b.HasKey("Key", "EntityLogicalName") + .HasName("pk_entity_changes_journals"); + + b.ToTable("entity_changes_journals", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Event", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("event_id"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("EventName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("event_name"); + + b.Property("Inserted") + .HasColumnType("timestamp with time zone") + .HasColumnName("inserted"); + + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("key"); + + b.Property("Payload") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("payload"); + + b.Property("Published") + .HasColumnType("boolean") + .HasColumnName("published"); + + b.HasKey("EventId") + .HasName("pk_events"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_events_key") + .HasFilter("key is not null"); + + b.HasIndex("Payload") + .HasDatabaseName("ix_events_payload"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Payload"), "gin"); + + b.ToTable("events", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Property("EytsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("EytsAwardedEmailsJobId") + .HasName("pk_eyts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_eyts_awarded_emails_jobs_executed_utc"); + + b.ToTable("eyts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.Property("EytsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("eyts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("EytsAwardedEmailsJobId", "PersonId") + .HasName("pk_eyts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_eyts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("eyts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Property("InductionCompletedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InductionCompletedEmailsJobId") + .HasName("pk_induction_completed_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_induction_completed_emails_jobs_executed_utc"); + + b.ToTable("induction_completed_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.Property("InductionCompletedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("induction_completed_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InductionCompletedEmailsJobId", "PersonId") + .HasName("pk_induction_completed_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_induction_completed_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("induction_completed_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("InternationalQtsAwardedEmailsJobId") + .HasName("pk_international_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_international_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("international_qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.Property("InternationalQtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("international_qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("InternationalQtsAwardedEmailsJobId", "PersonId") + .HasName("pk_international_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_international_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("international_qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.JourneyState", b => + { + b.Property("InstanceId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .HasColumnName("instance_id"); + + b.Property("Completed") + .HasColumnType("timestamp with time zone") + .HasColumnName("completed"); + + b.Property("Created") + .HasColumnType("timestamp with time zone") + .HasColumnName("created"); + + b.Property("State") + .IsRequired() + .HasColumnType("text") + .HasColumnName("state"); + + b.Property("Updated") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("user_id"); + + b.HasKey("InstanceId") + .HasName("pk_journey_states"); + + b.ToTable("journey_states", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", b => + { + b.Property("MandatoryQualificationProviderId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("mandatory_qualification_provider_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.HasKey("MandatoryQualificationProviderId") + .HasName("pk_mandatory_qualification_providers"); + + b.ToTable("mandatory_qualification_providers", (string)null); + + b.HasData( + new + { + MandatoryQualificationProviderId = new Guid("e28ea41d-408d-4c89-90cc-8b9b04ac68f5"), + Name = "University of Birmingham" + }, + new + { + MandatoryQualificationProviderId = new Guid("89f9a1aa-3d68-4985-a4ce-403b6044c18c"), + Name = "University of Leeds" + }, + new + { + MandatoryQualificationProviderId = new Guid("aa5c300e-3b7c-456c-8183-3520b3d55dca"), + Name = "University of Manchester" + }, + new + { + MandatoryQualificationProviderId = new Guid("f417e73e-e2ad-40eb-85e3-55865be7f6be"), + Name = "Mary Hare School / University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("fbf22e04-b274-4c80-aba8-79fb6a7a32ce"), + Name = "University of Edinburgh" + }, + new + { + MandatoryQualificationProviderId = new Guid("26204149-349c-4ad6-9466-bb9b83723eae"), + Name = "Liverpool John Moores University" + }, + new + { + MandatoryQualificationProviderId = new Guid("0c30f666-647c-4ea8-8883-0fc6010b56be"), + Name = "University of Oxford/Oxford Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d0e6d54c-5e90-438a-945d-f97388c2b352"), + Name = "University of Cambridge" + }, + new + { + MandatoryQualificationProviderId = new Guid("aec32252-ef25-452e-a358-34a04e03369c"), + Name = "University of Newcastle-upon-Tyne" + }, + new + { + MandatoryQualificationProviderId = new Guid("d9ee7054-7fde-4cfd-9a5e-4b99511d1b3d"), + Name = "University of Plymouth" + }, + new + { + MandatoryQualificationProviderId = new Guid("707d58ca-1953-413b-9a46-41e9b0be885e"), + Name = "University of Hertfordshire" + }, + new + { + MandatoryQualificationProviderId = new Guid("3fc648a7-18e4-49e7-8a4b-1612616b72d5"), + Name = "University of London" + }, + new + { + MandatoryQualificationProviderId = new Guid("374dceb8-8224-45b8-b7dc-a6b0282b1065"), + Name = "Bristol Polytechnic" + }, + new + { + MandatoryQualificationProviderId = new Guid("d4fc958b-21de-47ec-9f03-36ae237a1b11"), + Name = "University College, Swansea" + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DateOfBirth") + .HasColumnType("date") + .HasColumnName("date_of_birth"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtContactId") + .HasColumnType("uuid") + .HasColumnName("dqt_contact_id"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_first_name") + .UseCollation("case_insensitive"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_last_name") + .UseCollation("case_insensitive"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtMiddleName") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("dqt_middle_name") + .UseCollation("case_insensitive"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("EmailAddress") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("email_address") + .UseCollation("case_insensitive"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("first_name") + .UseCollation("case_insensitive"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("last_name") + .UseCollation("case_insensitive"); + + b.Property("MiddleName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("middle_name") + .UseCollation("case_insensitive"); + + b.Property("NationalInsuranceNumber") + .HasMaxLength(9) + .HasColumnType("character(9)") + .HasColumnName("national_insurance_number") + .IsFixedLength(); + + b.Property("Trn") + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("PersonId") + .HasName("pk_persons"); + + b.HasIndex("DqtContactId") + .IsUnique() + .HasDatabaseName("ix_persons_dqt_contact_id") + .HasFilter("dqt_contact_id is not null"); + + b.HasIndex("Trn") + .IsUnique() + .HasDatabaseName("ix_persons_trn") + .HasFilter("trn is not null"); + + b.ToTable("persons", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Property("QtsAwardedEmailsJobId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("AwardedToUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("awarded_to_utc"); + + b.Property("ExecutedUtc") + .HasColumnType("timestamp with time zone") + .HasColumnName("executed_utc"); + + b.HasKey("QtsAwardedEmailsJobId") + .HasName("pk_qts_awarded_emails_jobs"); + + b.HasIndex("ExecutedUtc") + .HasDatabaseName("ix_qts_awarded_emails_jobs_executed_utc"); + + b.ToTable("qts_awarded_emails_jobs", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.Property("QtsAwardedEmailsJobId") + .HasColumnType("uuid") + .HasColumnName("qts_awarded_emails_job_id"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email_address"); + + b.Property("EmailSent") + .HasColumnType("boolean") + .HasColumnName("email_sent"); + + b.Property("Personalization") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("personalization"); + + b.Property("Trn") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("character(7)") + .HasColumnName("trn") + .IsFixedLength(); + + b.HasKey("QtsAwardedEmailsJobId", "PersonId") + .HasName("pk_qts_awarded_emails_job_items"); + + b.HasIndex("Personalization") + .HasDatabaseName("ix_qts_awarded_emails_job_items_personalization"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Personalization"), "gin"); + + b.ToTable("qts_awarded_emails_job_items", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.Property("QualificationId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("qualification_id"); + + b.Property("CreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_on"); + + b.Property("DeletedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_on"); + + b.Property("DqtCreatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_created_on"); + + b.Property("DqtFirstSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_first_sync"); + + b.Property("DqtLastSync") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_last_sync"); + + b.Property("DqtModifiedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("dqt_modified_on"); + + b.Property("DqtQualificationId") + .HasColumnType("uuid") + .HasColumnName("dqt_qualification_id"); + + b.Property("DqtState") + .HasColumnType("integer") + .HasColumnName("dqt_state"); + + b.Property("PersonId") + .HasColumnType("uuid") + .HasColumnName("person_id"); + + b.Property("QualificationType") + .HasColumnType("integer") + .HasColumnName("qualification_type"); + + b.Property("UpdatedOn") + .HasColumnType("timestamp with time zone") + .HasColumnName("updated_on"); + + b.HasKey("QualificationId") + .HasName("pk_qualifications"); + + b.HasIndex("DqtQualificationId") + .IsUnique() + .HasDatabaseName("ix_qualifications_dqt_qualification_id") + .HasFilter("dqt_qualification_id is not null"); + + b.HasIndex("PersonId") + .HasDatabaseName("ix_qualifications_person_id"); + + b.ToTable("qualifications", (string)null); + + b.HasDiscriminator("QualificationType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.TrnRequest", b => + { + b.Property("TrnRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("trn_request_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("TrnRequestId")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("client_id"); + + b.Property("IdentityUserId") + .HasColumnType("uuid") + .HasColumnName("identity_user_id"); + + b.Property("LinkedToIdentity") + .HasColumnType("boolean") + .HasColumnName("linked_to_identity"); + + b.Property("RequestId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("request_id"); + + b.Property("TeacherId") + .HasColumnType("uuid") + .HasColumnName("teacher_id"); + + b.Property("TrnToken") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasColumnName("trn_token"); + + b.HasKey("TrnRequestId") + .HasName("pk_trn_requests"); + + b.HasIndex("ClientId", "RequestId") + .IsUnique() + .HasDatabaseName("ix_trn_requests_client_id_request_id"); + + b.ToTable("trn_requests", (string)null); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("name"); + + b.Property("UserType") + .HasColumnType("integer") + .HasColumnName("user_type"); + + b.HasKey("UserId") + .HasName("pk_users"); + + b.ToTable("users", (string)null); + + b.HasDiscriminator("UserType"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification"); + + b.Property("DqtMqEstablishmentId") + .HasColumnType("uuid") + .HasColumnName("dqt_mq_establishment_id"); + + b.Property("DqtSpecialismId") + .HasColumnType("uuid") + .HasColumnName("dqt_specialism_id"); + + b.Property("EndDate") + .HasColumnType("date") + .HasColumnName("end_date"); + + b.Property("ProviderId") + .HasColumnType("uuid") + .HasColumnName("mq_provider_id"); + + b.Property("Specialism") + .HasColumnType("integer") + .HasColumnName("mq_specialism"); + + b.Property("StartDate") + .HasColumnType("date") + .HasColumnName("start_date"); + + b.Property("Status") + .HasColumnType("integer") + .HasColumnName("mq_status"); + + b.HasDiscriminator().HasValue(0); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("ApiRoles") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("api_roles"); + + b.Property("OneLoginClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_client_id"); + + b.Property("OneLoginPrivateKeyPem") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("one_login_private_key_pem"); + + b.HasDiscriminator().HasValue(2); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.SystemUser", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.HasDiscriminator().HasValue(3); + + b.HasData( + new + { + UserId = new Guid("a81394d1-a498-46d8-af3e-e077596ab303"), + Active = true, + Name = "System", + UserType = 0 + }); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.User", b => + { + b.HasBaseType("TeachingRecordSystem.Core.DataStore.Postgres.Models.UserBase"); + + b.Property("AzureAdUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("azure_ad_user_id"); + + b.Property("DqtUserId") + .HasColumnType("uuid") + .HasColumnName("dqt_user_id"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("email") + .UseCollation("case_insensitive"); + + b.Property("Roles") + .IsRequired() + .HasColumnType("varchar[]") + .HasColumnName("roles"); + + b.HasIndex("AzureAdUserId") + .IsUnique() + .HasDatabaseName("ix_users_azure_ad_user_id"); + + b.HasDiscriminator().HasValue(1); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApiKey", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", "ApplicationUser") + .WithMany("ApiKeys") + .HasForeignKey("ApplicationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_api_key_application_user"); + + b.Navigation("ApplicationUser"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", "EytsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("EytsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_eyts_awarded_emails_job_items_eyts_awarded_emails_jobs_eyts"); + + b.Navigation("EytsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", "InductionCompletedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InductionCompletedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_induction_completed_emails_job_items_induction_completed_em"); + + b.Navigation("InductionCompletedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", "InternationalQtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("InternationalQtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_international_qts_awarded_emails_job_items_international_qt"); + + b.Navigation("InternationalQtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJobItem", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", "QtsAwardedEmailsJob") + .WithMany("JobItems") + .HasForeignKey("QtsAwardedEmailsJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qts_awarded_emails_job_items_qts_awarded_emails_jobs_qts_aw"); + + b.Navigation("QtsAwardedEmailsJob"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.Qualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_qualifications_person"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualification", b => + { + b.HasOne("TeachingRecordSystem.Core.DataStore.Postgres.Models.MandatoryQualificationProvider", null) + .WithMany() + .HasForeignKey("ProviderId") + .HasConstraintName("fk_qualifications_mandatory_qualification_provider"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.EytsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InductionCompletedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.InternationalQtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.QtsAwardedEmailsJob", b => + { + b.Navigation("JobItems"); + }); + + modelBuilder.Entity("TeachingRecordSystem.Core.DataStore.Postgres.Models.ApplicationUser", b => + { + b.Navigation("ApiKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.cs new file mode 100644 index 000000000..e2274e9f2 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/20240129115405_ApplicationUserOneLoginClientIdAndPem.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TeachingRecordSystem.Core.DataStore.Postgres.Migrations +{ + /// + public partial class ApplicationUserOneLoginClientIdAndPem : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "one_login_client_id", + table: "users", + type: "character varying(50)", + maxLength: 50, + nullable: true); + + migrationBuilder.AddColumn( + name: "one_login_private_key_pem", + table: "users", + type: "character varying(2000)", + maxLength: 2000, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "one_login_client_id", + table: "users"); + + migrationBuilder.DropColumn( + name: "one_login_private_key_pem", + table: "users"); + } + } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs index 0f1e491f8..6d879e504 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Migrations/TrsDbContextModelSnapshot.cs @@ -862,6 +862,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar[]") .HasColumnName("api_roles"); + b.Property("OneLoginClientId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .HasColumnName("one_login_client_id"); + + b.Property("OneLoginPrivateKeyPem") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("one_login_private_key_pem"); + b.HasDiscriminator().HasValue(2); }); diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs index e06d204c1..8b971c49d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/User.cs @@ -20,10 +20,13 @@ public class User : UserBase public class ApplicationUser : UserBase { + public const int OneLoginClientIdMaxLength = 50; public const string NameUniqueIndexName = "ix_users_application_user_name"; public required string[] ApiRoles { get; set; } public ICollection ApiKeys { get; } = null!; + public string? OneLoginClientId { get; set; } + public string? OneLoginPrivateKeyPem { get; set; } } public class SystemUser : UserBase diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/ApplicationUserUpdatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/ApplicationUserUpdatedEvent.cs index 5dc00f12a..dda1d8c42 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/ApplicationUserUpdatedEvent.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/ApplicationUserUpdatedEvent.cs @@ -14,4 +14,6 @@ public enum ApplicationUserUpdatedEventChanges None = 0, Name = 1 << 0, ApiRoles = 1 << 1, + OneLoginClientId = 1 << 2, + OneLoginPrivateKeyPem = 1 << 3 } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/ApplicationUser.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/ApplicationUser.cs index 8287cbfdc..0b79e138f 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/ApplicationUser.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/Models/ApplicationUser.cs @@ -5,11 +5,15 @@ public record ApplicationUser public required Guid UserId { get; init; } public required string Name { get; init; } public required string[] ApiRoles { get; set; } + public string? OneLoginClientId { get; set; } + public string? OneLoginPrivateKeyPem { get; set; } public static ApplicationUser FromModel(DataStore.Postgres.Models.ApplicationUser user) => new() { UserId = user.UserId, Name = user.Name, - ApiRoles = user.ApiRoles + ApiRoles = user.ApiRoles, + OneLoginClientId = user.OneLoginClientId, + OneLoginPrivateKeyPem = user.OneLoginPrivateKeyPem }; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml index 861accf65..ae1771487 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml @@ -41,7 +41,7 @@ { - @key.ApiKeyId + @key.ApiKeyId @if (key.Expires is DateTime expires) @@ -72,9 +72,13 @@
- Add API key + Add API key
+

One Login

+ + + Save changes diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs index a83c6f076..1295f5d7a 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/EditApplicationUser.cshtml.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Security.Cryptography; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -32,10 +33,21 @@ public class EditApplicationUserModel(TrsDbContext dbContext, TrsLinkGenerator l [Display(Name = "API keys")] public ApiKeyInfo[]? ApiKeys { get; set; } + [BindProperty] + [Display(Name = "Client ID")] + [MaxLength(ApplicationUser.OneLoginClientIdMaxLength, ErrorMessage = "One Login Client ID must be 50 characters or less")] + public string? OneLoginClientId { get; set; } + + [BindProperty] + [Display(Name = "Private Key PEM")] + public string? OneLoginPrivateKeyPem { get; set; } + public void OnGet() { Name = _user!.Name; ApiRoles = _user.ApiRoles; + OneLoginClientId = _user.OneLoginClientId; + OneLoginPrivateKeyPem = _user.OneLoginPrivateKeyPem; } public async Task OnPost() @@ -43,6 +55,28 @@ public async Task OnPost() // Sanitize roles var newApiRoles = ApiRoles!.Where(r => Core.ApiRoles.All.Contains(r)).ToArray(); + if (OneLoginClientId is not null && OneLoginPrivateKeyPem is null) + { + ModelState.AddModelError(nameof(OneLoginPrivateKeyPem), "One Login Private Key PEM is required if One Login Client ID is set"); + } + + if (OneLoginPrivateKeyPem is not null && OneLoginClientId is null) + { + ModelState.AddModelError(nameof(OneLoginClientId), "One Login Client ID is required if One Login Private Key PEM is set"); + } + + if (OneLoginPrivateKeyPem is not null && OneLoginClientId is not null) + { + try + { + RSA.Create().ImportFromPem(OneLoginPrivateKeyPem); + } + catch (ArgumentException) + { + ModelState.AddModelError(nameof(OneLoginPrivateKeyPem), "One Login Private Key PEM is invalid"); + } + } + if (!ModelState.IsValid) { return this.PageWithErrors(); @@ -52,7 +86,9 @@ public async Task OnPost() var changes = ApplicationUserUpdatedEventChanges.None | (Name != applicationUser.Name ? ApplicationUserUpdatedEventChanges.Name : 0) | - (!new HashSet(applicationUser.ApiRoles).SetEquals(new HashSet(newApiRoles)) ? ApplicationUserUpdatedEventChanges.ApiRoles : 0); + (!new HashSet(applicationUser.ApiRoles).SetEquals(new HashSet(newApiRoles)) ? ApplicationUserUpdatedEventChanges.ApiRoles : 0) | + (OneLoginClientId != applicationUser.OneLoginClientId ? ApplicationUserUpdatedEventChanges.OneLoginClientId : 0) | + (OneLoginPrivateKeyPem != applicationUser.OneLoginPrivateKeyPem ? ApplicationUserUpdatedEventChanges.OneLoginPrivateKeyPem : 0); if (changes != ApplicationUserUpdatedEventChanges.None) { @@ -60,6 +96,8 @@ public async Task OnPost() applicationUser.Name = Name!; applicationUser.ApiRoles = newApiRoles; + applicationUser.OneLoginClientId = OneLoginClientId; + applicationUser.OneLoginPrivateKeyPem = OneLoginPrivateKeyPem; var @event = new ApplicationUserUpdatedEvent() { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/Index.cshtml b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/Index.cshtml index d6d384273..f3fef85e7 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/Index.cshtml +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/ApplicationUsers/Index.cshtml @@ -19,7 +19,7 @@ { - @user.Name + @user.Name } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/ApplicationUserTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/ApplicationUserTests.cs new file mode 100644 index 000000000..2b0c8dfb2 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/ApplicationUserTests.cs @@ -0,0 +1,132 @@ +using System.Security.Cryptography; +using Microsoft.EntityFrameworkCore; +using TeachingRecordSystem.Core; + +namespace TeachingRecordSystem.SupportUi.EndToEndTests; + +public class ApplicationUserTests(HostFixture hostFixture) : TestBase(hostFixture) +{ + [Fact] + public async Task AddApplicationUser() + { + var applicationUserName = TestData.GenerateApplicationUserName(); + + await using var context = await HostFixture.CreateBrowserContext(); + var page = await context.NewPageAsync(); + + await page.GoToApplicationUsersPage(); + + await page.AssertOnApplicationUsersPage(); + + await page.ClickLinkForElementWithTestId("add-application-user"); + + await page.AssertOnAddApplicationUserPage(); + + await page.FillAsync("text=Name", applicationUserName); + + await page.ClickButton("Save"); + + var applicationUserId = await WithDbContext(async dbContext => + { + var applicationUser = await dbContext.ApplicationUsers.Where(u => u.Name == applicationUserName).SingleOrDefaultAsync(); + return applicationUser!.UserId; + }); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.AssertFlashMessage("Application user added"); + } + + [Fact] + public async Task EditApplicationUser() + { + var applicationUser = await TestData.CreateApplicationUser(); + var applicationUserId = applicationUser.UserId; + var newApplicationUserName = TestData.GenerateChangedApplicationUserName(applicationUser.Name); + var newOneLoginClientId = Guid.NewGuid().ToString(); + var newOneLoginPrivateKeyPem = TestCommon.TestData.GeneratePrivateKeyPem(); + + await using var context = await HostFixture.CreateBrowserContext(); + var page = await context.NewPageAsync(); + + await page.GoToApplicationUsersPage(); + + await page.AssertOnApplicationUsersPage(); + + await page.ClickLinkForElementWithTestId($"edit-application-user-{applicationUserId}"); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.FillAsync("text=Name", newApplicationUserName); + await page.SetCheckedAsync($"label:text-is('{ApiRoles.GetPerson}')", true); + await page.SetCheckedAsync($"label:text-is('{ApiRoles.UpdatePerson}')", true); + await page.FillAsync("text=Client ID", newOneLoginClientId); + await page.FillAsync("text=Private Key PEM", newOneLoginPrivateKeyPem); + + await page.ClickButton("Save changes"); + + await page.AssertOnApplicationUsersPage(); + + await page.AssertFlashMessage("Application user updated"); + } + + [Fact] + public async Task AddApiKey() + { + var applicationUser = await TestData.CreateApplicationUser(); + var applicationUserId = applicationUser.UserId; + var apiKey = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + + await using var context = await HostFixture.CreateBrowserContext(); + var page = await context.NewPageAsync(); + + await page.GoToApplicationUsersPage(); + + await page.AssertOnApplicationUsersPage(); + + await page.ClickLinkForElementWithTestId($"edit-application-user-{applicationUserId}"); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.ClickLinkForElementWithTestId("AddApiKey"); + + await page.AssertOnAddApiKeyPage(); + + await page.FillAsync("label:text-is('Key')", apiKey); + + await page.ClickButton("Save"); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.AssertFlashMessage("API key added"); + } + + [Fact] + public async Task EditApiKey() + { + var applicationUser = await TestData.CreateApplicationUser(); + var applicationUserId = applicationUser.UserId; + var apiKey = await TestData.CreateApiKey(applicationUser.UserId); + + await using var context = await HostFixture.CreateBrowserContext(); + var page = await context.NewPageAsync(); + + await page.GoToApplicationUsersPage(); + + await page.AssertOnApplicationUsersPage(); + + await page.ClickLinkForElementWithTestId($"edit-application-user-{applicationUserId}"); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.ClickLinkForElementWithTestId($"EditApiKey-{apiKey.ApiKeyId}"); + + await page.AssertOnEditApiKeyPage(apiKey.ApiKeyId); + + await page.ClickButton("Expire"); + + await page.AssertOnEditApplicationUserPage(applicationUserId); + + await page.AssertFlashMessage("API key expired"); + } +} diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs index aedc8e85c..dd3bca4e7 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/PageExtensions.cs @@ -41,6 +41,11 @@ public static async Task GoToUsersPage(this IPage page) await page.GotoAsync($"/users"); } + public static async Task GoToApplicationUsersPage(this IPage page) + { + await page.GotoAsync($"/application-users"); + } + public static Task ClickLinkForElementWithTestId(this IPage page, string testId) => page.GetByTestId(testId).ClickAsync(); @@ -265,6 +270,31 @@ public static async Task AssertOnEditUserPage(this IPage page, Guid userId) await page.WaitForUrlPathAsync($"/users/{userId}"); } + public static async Task AssertOnApplicationUsersPage(this IPage page) + { + await page.WaitForUrlPathAsync($"/application-users"); + } + + public static async Task AssertOnAddApplicationUserPage(this IPage page) + { + await page.WaitForUrlPathAsync($"/application-users/add"); + } + + public static async Task AssertOnEditApplicationUserPage(this IPage page, Guid applicationUserId) + { + await page.WaitForUrlPathAsync($"/application-users/{applicationUserId}"); + } + + public static async Task AssertOnAddApiKeyPage(this IPage page) + { + await page.WaitForUrlPathAsync($"/api-keys/add"); + } + + public static async Task AssertOnEditApiKeyPage(this IPage page, Guid apiKeyId) + { + await page.WaitForUrlPathAsync($"/api-keys/{apiKeyId}"); + } + public static async Task AssertFlashMessage(this IPage page, string expectedHeader) { Assert.Equal(expectedHeader, await page.InnerTextAsync($".govuk-notification-banner__heading:text-is('{expectedHeader}')")); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/TestBase.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/TestBase.cs index 8f842c193..4134dc936 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/TestBase.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.EndToEndTests/TestBase.cs @@ -1,3 +1,5 @@ +using Microsoft.EntityFrameworkCore; +using TeachingRecordSystem.Core.DataStore.Postgres; using TeachingRecordSystem.Core.DataStore.Postgres.Models; using TeachingRecordSystem.SupportUi.EndToEndTests.Infrastructure.Security; using TeachingRecordSystem.TestCommon; @@ -17,6 +19,13 @@ public TestBase(HostFixture hostFixture) public TestData TestData => HostFixture.Services.GetRequiredService(); + public virtual async Task WithDbContext(Func> action) + { + var dbContextFactory = HostFixture.Services.GetRequiredService>(); + await using var dbContext = await dbContextFactory.CreateDbContextAsync(); + return await action(dbContext); + } + protected void SetCurrentUser(User user) { var currentUserProvider = HostFixture.Services.GetRequiredService(); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ApplicationUsers/EditApplicationUserTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ApplicationUsers/EditApplicationUserTests.cs index 580b2a307..e5dc9db30 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ApplicationUsers/EditApplicationUserTests.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.SupportUi.Tests/PageTests/ApplicationUsers/EditApplicationUserTests.cs @@ -42,7 +42,7 @@ public async Task Get_UserDoesNotExist_ReturnsNotFound() public async Task Get_ValidRequest_RendersExpectedContent() { // Arrange - var applicationUser = await TestData.CreateApplicationUser(apiRoles: [ApiRoles.GetPerson, ApiRoles.UpdatePerson]); + var applicationUser = await TestData.CreateApplicationUser(apiRoles: [ApiRoles.GetPerson, ApiRoles.UpdatePerson], hasOneLoginSettings: true); var apiKeyUnexpired = await TestData.CreateApiKey(applicationUser.UserId, expired: false); var apiKeyExpired = await TestData.CreateApiKey(applicationUser.UserId, expired: true); @@ -73,6 +73,9 @@ public async Task Get_ValidRequest_RendersExpectedContent() var expiry = row.GetElementByTestId("Expiry")?.TextContent?.Trim(); Assert.Equal(apiKeyExpired.Expires!.Value.ToString("dd/MM/yyyy HH:mm"), expiry); }); + + Assert.Equal(applicationUser.OneLoginClientId, doc.GetElementById("OneLoginClientId")?.GetAttribute("value")); + Assert.Equal(applicationUser.OneLoginPrivateKeyPem, doc.GetElementById("OneLoginPrivateKeyPem")?.TextContent?.Trim()); } [Fact] @@ -123,7 +126,7 @@ public async Task Post_UserDoesNotExist_ReturnsNotFound() } [Fact] - public async Task Post_NameNotProvider_RendersError() + public async Task Post_NameNotProvided_RendersError() { // Arrange var applicationUser = await TestData.CreateApplicationUser(apiRoles: []); @@ -169,20 +172,93 @@ public async Task Post_NameTooLong_RendersError() } [Fact] - public async Task Post_ValidRequest_UpdatesNameAndRolesCreatesEventAndRedirectsWithFlashMessage() + public async Task Post_OneLoginClientIdButNoPem_RendersError() + { + // Arrange + var applicationUser = await TestData.CreateApplicationUser(apiRoles: []); + var oneLoginClientId = Guid.NewGuid().ToString(); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/application-users/{applicationUser.UserId}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "Name", applicationUser.Name }, + { "OneLoginClientId", oneLoginClientId } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "OneLoginPrivateKeyPem", "One Login Private Key PEM is required if One Login Client ID is set"); + } + + [Fact] + public async Task Post_OneLoginClientIdTooLong_RendersError() + { + // Arrange + var applicationUser = await TestData.CreateApplicationUser(apiRoles: []); + var oneLoginClientId = new string('x', ApplicationUser.OneLoginClientIdMaxLength + 1); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/application-users/{applicationUser.UserId}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "Name", applicationUser.Name }, + { "OneLoginClientId", oneLoginClientId } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "OneLoginClientId", "One Login Client ID must be 50 characters or less"); + } + + [Fact] + public async Task Post_OneLoginPrivateKeyPemButNoClientId_RendersError() { // Arrange var applicationUser = await TestData.CreateApplicationUser(apiRoles: []); + var oneLoginPrivateKeyPem = TestData.GeneratePrivateKeyPem(); + + var request = new HttpRequestMessage(HttpMethod.Post, $"/application-users/{applicationUser.UserId}") + { + Content = new FormUrlEncodedContentBuilder() + { + { "Name", applicationUser.Name }, + { "OneLoginPrivateKeyPem", oneLoginPrivateKeyPem! } + } + }; + + // Act + var response = await HttpClient.SendAsync(request); + + // Assert + await AssertEx.HtmlResponseHasError(response, "OneLoginClientId", "One Login Client ID is required if One Login Private Key PEM is set"); + } + + [Fact] + public async Task Post_ValidRequest_UpdatesNameAndRolesAndOneLoginSettinsAndCreatesEventAndRedirectsWithFlashMessage() + { + // Arrange + var applicationUser = await TestData.CreateApplicationUser(apiRoles: [], hasOneLoginSettings: false); var originalName = applicationUser.Name; var newName = TestData.GenerateChangedApplicationUserName(originalName); var newRoles = new[] { ApiRoles.GetPerson, ApiRoles.UpdatePerson }; + var oneLoginClientId = Guid.NewGuid().ToString(); + var oneLoginPrivateKeyPem = TestData.GeneratePrivateKeyPem(); var request = new HttpRequestMessage(HttpMethod.Post, $"/application-users/{applicationUser.UserId}") { Content = new FormUrlEncodedContentBuilder() { { "Name", newName }, - { "ApiRoles", newRoles } + { "ApiRoles", newRoles }, + { "OneLoginClientId", oneLoginClientId }, + { "OneLoginPrivateKeyPem", oneLoginPrivateKeyPem! } } }; @@ -211,7 +287,11 @@ await WithDbContext(async dbContext => Assert.Equal(newName, applicationUserUpdatedEvent.ApplicationUser.Name); Assert.True(applicationUserUpdatedEvent.ApplicationUser.ApiRoles.SequenceEqual(newRoles)); Assert.Empty(applicationUserUpdatedEvent.OldApplicationUser.ApiRoles); - Assert.Equal(ApplicationUserUpdatedEventChanges.ApiRoles | ApplicationUserUpdatedEventChanges.Name, applicationUserUpdatedEvent.Changes); + Assert.Null(applicationUserUpdatedEvent.OldApplicationUser.OneLoginClientId); + Assert.Null(applicationUserUpdatedEvent.OldApplicationUser.OneLoginPrivateKeyPem); + Assert.Equal(oneLoginClientId, applicationUserUpdatedEvent.ApplicationUser.OneLoginClientId); + Assert.Equal(oneLoginPrivateKeyPem, applicationUserUpdatedEvent.ApplicationUser.OneLoginPrivateKeyPem); + Assert.Equal(ApplicationUserUpdatedEventChanges.ApiRoles | ApplicationUserUpdatedEventChanges.Name | ApplicationUserUpdatedEventChanges.OneLoginClientId | ApplicationUserUpdatedEventChanges.OneLoginPrivateKeyPem, applicationUserUpdatedEvent.Changes); }); var redirectResponse = await response.FollowRedirect(HttpClient); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs index f0fea74ef..db1db6d57 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.TestCommon/TestData.CreateApplicationUser.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using TeachingRecordSystem.Core.DataStore.Postgres.Models; using TeachingRecordSystem.Core.Events; @@ -7,18 +8,28 @@ public partial class TestData { public async Task CreateApplicationUser( string? name = null, - string[]? apiRoles = null) + string[]? apiRoles = null, + bool? hasOneLoginSettings = false) { var user = await WithDbContext(async dbContext => { name ??= GenerateApplicationUserName(); apiRoles ??= []; + string? oneLoginClientId = null; + string? oneLoginPrivateKeyPem = null; + if (hasOneLoginSettings == true) + { + oneLoginClientId = Guid.NewGuid().ToString(); + oneLoginPrivateKeyPem = GeneratePrivateKeyPem(); + } var user = new ApplicationUser() { Name = name, UserId = Guid.NewGuid(), - ApiRoles = apiRoles + ApiRoles = apiRoles, + OneLoginClientId = oneLoginClientId, + OneLoginPrivateKeyPem = oneLoginPrivateKeyPem }; dbContext.ApplicationUsers.Add(user); @@ -39,4 +50,9 @@ public async Task CreateApplicationUser( return user; } + + public static string GeneratePrivateKeyPem() + { + return RSA.Create().ExportRSAPrivateKeyPem(); + } }