diff --git a/.github/workflows/api-e2e-mssql-multitenant.yml b/.github/workflows/api-e2e-mssql-multitenant.yml index 1280b7c56..d4d097ae1 100644 --- a/.github/workflows/api-e2e-mssql-multitenant.yml +++ b/.github/workflows/api-e2e-mssql-multitenant.yml @@ -40,9 +40,6 @@ jobs: mkdir ../../Docker/Application cp -r ../EdFi.Ods.AdminApi ../../Docker/Application - - name: Copy admin console folder to docker context - run: cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application - - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application diff --git a/.github/workflows/api-e2e-mssql-singletenant.yml b/.github/workflows/api-e2e-mssql-singletenant.yml index 0756042dc..71deb318a 100644 --- a/.github/workflows/api-e2e-mssql-singletenant.yml +++ b/.github/workflows/api-e2e-mssql-singletenant.yml @@ -40,9 +40,6 @@ jobs: mkdir ../../Docker/Application cp -r ../EdFi.Ods.AdminApi ../../Docker/Application - - name: Copy admin console folder to docker context - run: cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application - - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-multitenant.yml b/.github/workflows/api-e2e-pgsql-multitenant.yml index bc6b1dafc..918835e10 100644 --- a/.github/workflows/api-e2e-pgsql-multitenant.yml +++ b/.github/workflows/api-e2e-pgsql-multitenant.yml @@ -40,9 +40,6 @@ jobs: mkdir ../../Docker/Application cp -r ../EdFi.Ods.AdminApi ../../Docker/Application - - name: Copy admin console folder to docker context - run: cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application - - name: Copy admin api common folder to docker context run: cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application diff --git a/.github/workflows/api-e2e-pgsql-singletenant.yml b/.github/workflows/api-e2e-pgsql-singletenant.yml index 91410a675..d6fd769ad 100644 --- a/.github/workflows/api-e2e-pgsql-singletenant.yml +++ b/.github/workflows/api-e2e-pgsql-singletenant.yml @@ -39,7 +39,6 @@ jobs: run: | mkdir ../../Docker/Application cp -r ../EdFi.Ods.AdminApi ../../Docker/Application - cp -r ../EdFi.Ods.AdminApi.AdminConsole ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.Common ../../Docker/Application cp -r ../EdFi.Ods.AdminApi.V1 ../../Docker/Application diff --git a/Application/Ed-Fi-ODS-AdminApi.sln b/Application/Ed-Fi-ODS-AdminApi.sln index 0359a68df..8162d5eb7 100644 --- a/Application/Ed-Fi-ODS-AdminApi.sln +++ b/Application/Ed-Fi-ODS-AdminApi.sln @@ -20,8 +20,6 @@ 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("{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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common", "EdFi.Ods.AdminApi.Common\EdFi.Ods.AdminApi.Common.csproj", "{C9C86866-562B-4EA3-9AAC-F3297F0754D6}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EdFi.Ods.AdminApi.Common.UnitTests", "EdFi.Ods.AdminApi.Common.UnitTests\EdFi.Ods.AdminApi.Common.UnitTests.csproj", "{D0B566A2-000E-48FC-9CD0-702F7A00CA76}" @@ -60,14 +58,6 @@ Global {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|Any CPU.Build.0 = Release|Any CPU {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.ActiveCfg = Release|Any CPU {73259EC2-4AA0-40C2-9C60-8AB1BF369CF5}.Release|x64.Build.0 = Release|Any CPU - {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Debug|x64.ActiveCfg = Debug|Any CPU - {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Debug|x64.Build.0 = Debug|Any CPU - {0F34C4F6-F7A2-442A-9E54-FCBD9A00F914}.Release|Any CPU.ActiveCfg = Release|Any CPU - {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 {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C9C86866-562B-4EA3-9AAC-F3297F0754D6}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole.UnitTests/EdFi.Ods.AdminApi.AdminConsole.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.AdminConsole.UnitTests/EdFi.Ods.AdminApi.AdminConsole.UnitTests.csproj deleted file mode 100644 index 44e89119d..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole.UnitTests/EdFi.Ods.AdminApi.AdminConsole.UnitTests.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - net8.0 - enable - enable - - false - true - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj b/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj deleted file mode 100644 index 624421407..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/EdFi.Ods.AdminApi.AdminConsole.csproj +++ /dev/null @@ -1,54 +0,0 @@ - - - - net8.0 - enable - enable - true - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/ReadTenants.cs deleted file mode 100644 index 46a382f68..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/ReadTenants.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Services.Tenants; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure; -using EdFi.Ods.AdminApi.Common.Infrastructure.Security; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Caching.Memory; - -namespace EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; - -public class ReadTenants : IFeature -{ - public void MapEndpoints(IEndpointRouteBuilder endpoints) - { - AdminApiEndpointBuilder.MapGet(endpoints, "/tenants", GetTenantsAsync) - .BuildForVersions(AdminApiVersions.AdminConsole); - - AdminApiEndpointBuilder.MapGet(endpoints, "/tenants/{tenantId}", GetTenantsByTenantIdAsync) - .BuildForVersions(AdminApiVersions.AdminConsole); - } - - public static async Task GetTenantsAsync(IAdminConsoleTenantsService adminConsoleTenantsService, IMemoryCache memoryCache) - { - var tenants = await adminConsoleTenantsService.GetTenantsAsync(true); - return Results.Ok(tenants); - } - - public static async Task GetTenantsByTenantIdAsync(IAdminConsoleTenantsService adminConsoleTenantsService, - IMemoryCache memoryCache, int tenantId) - { - var tenant = await adminConsoleTenantsService.GetTenantByTenantIdAsync(tenantId); - if (tenant != null) - return Results.Ok(tenant); - return Results.NotFound(); - } -} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/TenantModel.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/TenantModel.cs deleted file mode 100644 index c71568403..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Features/Tenants/TenantModel.cs +++ /dev/null @@ -1,19 +0,0 @@ -// 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.Dynamic; -using EdFi.Ods.AdminApi.Common.Constants; -using Swashbuckle.AspNetCore.Annotations; - -namespace EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; - -[SwaggerSchema] -public class TenantModel -{ - [SwaggerSchema(Description = AdminConsoleConstants.TenantIdDescription, Nullable = false)] - public int TenantId { get; set; } - [SwaggerSchema(Description = AdminConsoleConstants.TenantIdDescription, Nullable = false, Format = "{\r\n \"name\": \"Tenant1\",\r\n \"edfiApiDiscoveryUrl\": \"https://api.ed-fi.org/v7.2/api\"\r\n }")] - public ExpandoObject? Document { get; set; } -} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Helper/AdminConsoleFeatureHelper.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Helper/AdminConsoleFeatureHelper.cs deleted file mode 100644 index 0bdd78898..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Helper/AdminConsoleFeatureHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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.Reflection; -using EdFi.Ods.AdminApi.Common.Features; -using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; - -namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Helper; - -public static class AdminConsoleFeatureHelper -{ - public static List GetFeatures() => FeatureHelper.GetFeatures(Assembly.GetExecutingAssembly()); -} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/IMarkerForEdFiAdminConsoleManagement.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/IMarkerForEdFiAdminConsoleManagement.cs deleted file mode 100644 index 8b4dfe60d..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/IMarkerForEdFiAdminConsoleManagement.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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; - -public interface IMarkerForEdFiAdminConsoleManagement -{ -} - diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/TenantBackgroundService.cs b/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/TenantBackgroundService.cs deleted file mode 100644 index 79c7ef230..000000000 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/TenantBackgroundService.cs +++ /dev/null @@ -1,56 +0,0 @@ -// 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.Services.Tenants; -using EdFi.Ods.AdminApi.Common.Settings; -using log4net; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; - -public class TenantBackgroundService : BackgroundService -{ - private readonly IDisposable _optionsChangedListener; - private static readonly ILog _log = LogManager.GetLogger(typeof(TenantService)); - private readonly IServiceScopeFactory _serviceScopeFactory; - public TenantBackgroundService(IOptionsMonitor optionsMonitor, IServiceScopeFactory serviceScopeFactory) - { - _serviceScopeFactory = serviceScopeFactory; - _optionsChangedListener = optionsMonitor.OnChange(async (opt) => await OnAppSettingsChangedAsync())!; - } - - private async Task OnAppSettingsChangedAsync() - { - using IServiceScope scope = _serviceScopeFactory.CreateScope(); - _log.Info("The appsettings file has been modified"); - - IAdminConsoleTenantsService scopedProcessingService = - scope.ServiceProvider.GetRequiredService(); - - await scopedProcessingService.InitializeTenantsAsync(); - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); - } - } - - public override async Task StopAsync(CancellationToken cancellationToken) - { - _log.Info("Stopping background"); - await base.StopAsync(cancellationToken); - } - - public override void Dispose() - { - _optionsChangedListener.Dispose(); - base.Dispose(); - } -} diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleConstants.cs deleted file mode 100644 index ba0eee5eb..000000000 --- a/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleConstants.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Common.Constants; - -public static class AdminConsoleConstants -{ - public const string AdminConsoleSettingsKey = "AdminConsoleSettings"; - - - public const string EnableCorsKey = "CorsSettings:EnableCors"; - - public const string AllowedOriginsCorsKey = "CorsSettings:AllowedOrigins"; - - public const string CorsPolicyName = "allowAllCorsPolicyName"; - - public const string TenantsCacheKey = "adminconsole.tenants"; - - public const string TenantIdDescription = "Admin API Tenant Id"; - - public const string TenantDocumentDescription = "Tenant Document as JSON object"; -} diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleValidationConstants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleValidationConstants.cs deleted file mode 100644 index f61c16aaf..000000000 --- a/Application/EdFi.Ods.AdminApi.Common/Constants/AdminConsoleValidationConstants.cs +++ /dev/null @@ -1,20 +0,0 @@ -// 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 System.Text; -using System.Threading.Tasks; - -namespace EdFi.Ods.AdminApi.Common.Constants; - -public static class AdminConsoleValidationConstants -{ - public const string OdsIntanceIdIsNotValid = "The instance id is not valid."; - public const string OdsIntanceIdStatusIsNotCompleted = "The instance cannot be deleted because it is not in a COMPLETED status."; - public const string OdsInstanceIdStatusIsNotPendingDelete = "The instance status is invalid; it is not marked as 'Pending Delete'."; - public const string OdsIntanceIdStatusIsPendingRename = "The instance cannot be set rename failed because it is not in a PENDING_RENAME status."; -} diff --git a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs index c1c34507b..36c03fb1a 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Constants/Constants.cs @@ -5,6 +5,14 @@ namespace EdFi.Ods.AdminApi.Common.Constants; +public class Constants +{ + public const string TenantsCacheKey = "tenants"; + public const string TenantNameDescription = "Admin API Tenant Name"; + public const string TenantConnectionStringDescription = "Tenant connection strings"; + public const string DefaultTenantName = "default"; +} + public enum AdminApiMode { V2, diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs index 5f0d5d7ac..c00e4bf76 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/AdminApiVersions.cs @@ -17,7 +17,6 @@ public class AdminApiVersions public static readonly AdminApiVersion V1 = new(1.1, "v1"); public static readonly AdminApiVersion V2 = new(2.0, "v2"); - public static readonly AdminApiVersion AdminConsole = new(1.0, "adminconsole"); private static ApiVersionSet? _versionSet; public static void Initialize(WebApplication app) @@ -25,21 +24,11 @@ public static void Initialize(WebApplication app) if (_isInitialized) throw new InvalidOperationException("Versions are already initialized"); - if (app.Configuration.GetValue("AppSettings:EnableAdminConsoleAPI")) - { - _versionSet = app.NewApiVersionSet() - .HasApiVersion(V1.Version) - .HasApiVersion(V2.Version) - .HasApiVersion(AdminConsole.Version) - .Build(); - } - else - { - _versionSet = app.NewApiVersionSet() - .HasApiVersion(V1.Version) - .HasApiVersion(V2.Version) - .Build(); - } + _versionSet = app.NewApiVersionSet() + .HasApiVersion(V1.Version) + .HasApiVersion(V2.Version) + .Build(); + _isInitialized = true; } diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs index 5fc0b06e0..b9ba02d5c 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/Helpers/ConnectionStringHelper.cs @@ -46,4 +46,39 @@ ex is FormatException || } return result; } + + public static (string? Host, string? Database) GetHostAndDatabase(string databaseEngine, string? connectionString) + { + if (databaseEngine.Equals(DatabaseEngineEnum.SqlServer, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + var builder = new SqlConnectionStringBuilder(connectionString); + return (builder.DataSource, builder.InitialCatalog); + } + catch (Exception ex) when ( + ex is ArgumentException or + FormatException or + KeyNotFoundException) + { + _log.Error(ex); + return (null, null); + } + } + else if (databaseEngine.Equals(DatabaseEngineEnum.PostgreSql, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + var builder = new NpgsqlConnectionStringBuilder(connectionString); + return (builder.Host, builder.Database); + } + catch (ArgumentException ex) + { + _log.Error(ex); + return (null, null); + } + } + + return (null, null); + } } diff --git a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs index fb9d28a7e..83ddfd1f3 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Infrastructure/MultiTenancy/TenantIdentificationMiddleware.cs @@ -80,18 +80,11 @@ public async Task InvokeAsync(HttpContext context, RequestDelegate next) } else { - if (_options.Value.EnableAdminConsoleAPI) + if (!context.Request.Path.Value!.Contains("adminconsole/tenants") && + context.Request.Method != "GET" && + !context.Request.Path.Value.Contains("health", StringComparison.InvariantCultureIgnoreCase)) { - if (!context.Request.Path.Value!.Contains("adminconsole/tenants") && - context.Request.Method != "GET" && - !context.Request.Path.Value.Contains("health", StringComparison.InvariantCultureIgnoreCase)) - { - ThrowTenantValidationError("Tenant header is missing (adminconsole)"); - } - } - else if (!NonFeatureEndpoints()) - { - ThrowTenantValidationError("Tenant header is missing"); + ThrowTenantValidationError("Tenant header is missing (adminconsole)"); } } } @@ -101,13 +94,6 @@ bool RequestFromSwagger() => (context.Request.Path.Value != null && context.Request.Path.Value.Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) || context.Request.Headers.Referer.FirstOrDefault(x => x != null && x.ToLower().Contains("swagger", StringComparison.InvariantCultureIgnoreCase)) != null; - bool NonFeatureEndpoints() => context.Request.Path.Value != null && - (context.Request.Path.Value.Contains("health", StringComparison.InvariantCultureIgnoreCase) - || context.Request.Path.Value.Equals("/") - || context.Request.Path.Value.Contains("connect", StringComparison.InvariantCultureIgnoreCase) - || (context.Request.PathBase.HasValue && !context.Request.Path.HasValue) - || (context.Request.Path.StartsWithSegments(new PathString("/.well-known")))); - void ThrowTenantValidationError(string errorMessage) { throw new ValidationException([new ValidationFailure("Tenant", errorMessage)]); diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/AdminConsoleSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/AdminConsoleSettings.cs deleted file mode 100644 index 3c2487ad8..000000000 --- a/Application/EdFi.Ods.AdminApi.Common/Settings/AdminConsoleSettings.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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.Common.Settings; - -public class AdminConsoleSettings : IEncryptionKeySettings -{ - public CorsSettings CorsSettings { get; set; } = new CorsSettings(); - public string EncryptionKey { get; set; } = string.Empty; - public string VendorCompany { get; set; } = string.Empty; - public string ApplicationName { get; set; } = string.Empty; -} diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs index c94148a75..635140d3c 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/AppSettings.cs @@ -1,38 +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. - +// 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.Common.Settings; public class AppSettingsFile { public required AppSettings AppSettings { get; set; } public required SwaggerSettings SwaggerSettings { get; set; } - public required AdminConsoleSettings AdminConsoleSettings { get; set; } - public string? EdFiApiDiscoveryUrl { get; set; } - public string[] ConnectionStrings { get; set; } = []; + public Dictionary ConnectionStrings { get; set; } = []; public Dictionary Tenants { get; set; } = new(StringComparer.OrdinalIgnoreCase); public required TestingSettings Testing { get; set; } } - -public class AppSettings -{ - public int DefaultPageSizeOffset { get; set; } - public int DefaultPageSizeLimit { get; set; } + +public class AppSettings +{ + public int DefaultPageSizeOffset { get; set; } + public int DefaultPageSizeLimit { get; set; } public string? DatabaseEngine { get; set; } - public string? EncryptionKey { get; set; } - public bool MultiTenancy { get; set; } - public bool PreventDuplicateApplications { get; set; } - public bool EnableAdminConsoleAPI { get; set; } - public bool EnableApplicationResetEndpoint { get; set; } + public string? EncryptionKey { get; set; } + public bool MultiTenancy { get; set; } + public bool PreventDuplicateApplications { get; set; } + public bool EnableApplicationResetEndpoint { get; set; } public string? AdminApiMode { get; set; } -} - -public class SwaggerSettings -{ - public bool EnableSwagger { get; set; } - public string? DefaultTenant { get; set; } +} + +public class SwaggerSettings +{ + public bool EnableSwagger { get; set; } + public string? DefaultTenant { get; set; } } @@ -53,4 +50,4 @@ public class GeneralRule public string Period { get; set; } = string.Empty; public int Limit { get; set; } } -} +} diff --git a/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs index e0595875f..284a4758a 100644 --- a/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs +++ b/Application/EdFi.Ods.AdminApi.Common/Settings/TenantSettings.cs @@ -12,7 +12,5 @@ public class TenantsSection public class TenantSettings { - public Dictionary ConnectionStrings { get; set; } = new(StringComparer.OrdinalIgnoreCase); - - public string EdFiApiDiscoveryUrl { get; set; } = string.Empty; -} + public Dictionary ConnectionStrings { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs new file mode 100644 index 000000000..2653c6ee1 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/AddTenantCommandTests.cs @@ -0,0 +1,132 @@ +// 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.IO; +using System.Text.Json; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class AddTenantCommandTests +{ + private string _testFilePath; + + private static string GetTestFilePath(string testName) + { + var dir = Path.Combine(Path.GetTempPath(), "AdminApiTests"); + Directory.CreateDirectory(dir); + return Path.Combine(dir, $"{testName}_{Guid.NewGuid()}.json"); + } + + private void CopyTestSettings(string destPath, string sourceFile = "testsappsettings.json") + { + var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, sourceFile); + File.Copy(sourcePath, destPath, true); + } + + [TearDown] + public void Cleanup() + { + if (!string.IsNullOrEmpty(_testFilePath) && File.Exists(_testFilePath)) + { + File.Delete(_testFilePath); + } + } + + [Test] + public void ShouldThrowWhenAppSettingsIsEmpty() + { + _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenAppSettingsIsEmpty)); + File.WriteAllText(_testFilePath, string.Empty); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new AddTenantCommand(provider); + + var model = new TestAddTenantModel("newtenant", "sec", "adm"); + + var ex = Should.Throw(() => command.Execute(model)); + ex.Message.ShouldBe("appsettings.json contains invalid JSON."); + } + + [Test] + public void ShouldThrowWhenTenantsSectionMissing() + { + _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenTenantsSectionMissing)); + var json = @"{ ""ConnectionStrings"": { ""EdFi_Admin"": ""a"", ""EdFi_Security"": ""b"" } }"; + File.WriteAllText(_testFilePath, json); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new AddTenantCommand(provider); + + var model = new TestAddTenantModel("newtenant", "sec", "adm"); + + var ex = Should.Throw(() => command.Execute(model)); + ex.Message.ShouldBe("Tenants section missing in appsettings.json."); + } + + [Test] + public void ShouldThrowWhenTenantAlreadyExists() + { + _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenTenantAlreadyExists)); + CopyTestSettings(_testFilePath); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new AddTenantCommand(provider); + + var model = new TestAddTenantModel("tenant1", "sec", "adm"); + + var ex = Should.Throw(() => command.Execute(model)); + ex.Message.ShouldBe("Tenant 'tenant1' already exists."); + } + + [Test] + public void ShouldThrowWhenAppSettingsContainsInvalidJson() + { + _testFilePath = GetTestFilePath(nameof(ShouldThrowWhenAppSettingsContainsInvalidJson)); + File.WriteAllText(_testFilePath, "{ invalid json }"); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new AddTenantCommand(provider); + + var model = new TestAddTenantModel("newtenant", "sec", "adm"); + + var ex = Should.Throw(() => command.Execute(model)); + ex.Message.ShouldBe("appsettings.json contains invalid JSON."); + } + + [Test] + public void ShouldAddNewTenantWhenValid() + { + _testFilePath = GetTestFilePath(nameof(ShouldAddNewTenantWhenValid)); + CopyTestSettings(_testFilePath); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new AddTenantCommand(provider); + + var model = new TestAddTenantModel("newtenant", "sec-conn", "adm-conn"); + + command.Execute(model); + + var updatedJson = File.ReadAllText(_testFilePath); + using var doc = JsonDocument.Parse(updatedJson); + var tenants = doc.RootElement.GetProperty("Tenants"); + tenants.TryGetProperty("newtenant", out var newTenant).ShouldBeTrue(); + var connStrings = newTenant.GetProperty("ConnectionStrings"); + connStrings.GetProperty("EdFi_Security").GetString().ShouldBe("sec-conn"); + connStrings.GetProperty("EdFi_Admin").GetString().ShouldBe("adm-conn"); + } + + private class TestAddTenantModel(string tenantName, string sec, string adm) : IAddTenantModel + { + public string TenantName { get; } = tenantName; + public string EdFiSecurityConnectionString { get; } = sec; + public string EdFiAdminConnectionString { get; } = adm; + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs new file mode 100644 index 000000000..84e42267d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/Database/CommandTests/DeleteTenantCommandTests.cs @@ -0,0 +1,113 @@ +// 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.IO; +using System.Text.Json; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.DBTests.Database.CommandTests; + +[TestFixture] +public class DeleteTenantCommandTests +{ + private string _testFilePath = string.Empty; + + private static string GetTestFilePath(string testName) + { + var dir = Path.Combine(Path.GetTempPath(), "AdminApiTests"); + Directory.CreateDirectory(dir); + return Path.Combine(dir, $"{testName}_{Guid.NewGuid()}.json"); + } + + private void CopyTestSettings(string destPath, string sourceFile = "testsappsettings.json") + { + var sourcePath = Path.Combine(TestContext.CurrentContext.TestDirectory, sourceFile); + File.Copy(sourcePath, destPath, true); + } + + [TearDown] + public void Cleanup() + { + if (_testFilePath is not null && File.Exists(_testFilePath)) + { + File.Delete(_testFilePath); + } + } + + [Test] + public void Should_throw_when_appsettings_is_empty() + { + _testFilePath = GetTestFilePath(nameof(Should_throw_when_appsettings_is_empty)); + File.WriteAllText(_testFilePath, string.Empty); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new DeleteTenantCommand(provider); + + var ex = Should.Throw(() => command.Execute("tenant1")); + ex.Message.ShouldBe("appsettings.json contains invalid JSON."); + } + + [Test] + public void Should_throw_when_tenants_section_missing() + { + _testFilePath = GetTestFilePath(nameof(Should_throw_when_tenants_section_missing)); + var json = @"{ ""ConnectionStrings"": { ""EdFi_Admin"": ""a"", ""EdFi_Security"": ""b"" } }"; + File.WriteAllText(_testFilePath, json); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new DeleteTenantCommand(provider); + + var ex = Should.Throw(() => command.Execute("tenant1")); + ex.Message.ShouldBe("Tenants section missing in appsettings.json."); + } + + [Test] + public void Should_throw_when_tenant_does_not_exist() + { + _testFilePath = GetTestFilePath(nameof(Should_throw_when_tenant_does_not_exist)); + CopyTestSettings(_testFilePath); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new DeleteTenantCommand(provider); + + var ex = Should.Throw(() => command.Execute("notarealtenant")); + ex.Message.ShouldBe("Tenant 'notarealtenant' does not exist."); + } + + [Test] + public void Should_throw_when_appsettings_contains_invalid_json() + { + _testFilePath = GetTestFilePath(nameof(Should_throw_when_appsettings_contains_invalid_json)); + File.WriteAllText(_testFilePath, "{ invalid json }"); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new DeleteTenantCommand(provider); + + var ex = Should.Throw(() => command.Execute("tenant1")); + ex.Message.ShouldBe("appsettings.json contains invalid JSON."); + } + + [Test] + public void Should_delete_tenant_when_valid() + { + _testFilePath = GetTestFilePath(nameof(Should_delete_tenant_when_valid)); + CopyTestSettings(_testFilePath); + + var provider = new FileSystemAppSettingsFileProvider(_testFilePath); + var command = new DeleteTenantCommand(provider); + + command.Execute("tenant1"); + + var updatedJson = File.ReadAllText(_testFilePath); + using var doc = JsonDocument.Parse(updatedJson); + var tenants = doc.RootElement.GetProperty("Tenants"); + tenants.TryGetProperty("tenant1", out _).ShouldBeFalse(); + tenants.TryGetProperty("tenant2", out _).ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj b/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj index 33a6a7f49..2feef5fbe 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj +++ b/Application/EdFi.Ods.AdminApi.DBTests/EdFi.Ods.AdminApi.DBTests.csproj @@ -31,6 +31,9 @@ + + Always + Always diff --git a/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json b/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json index 25bce9ed3..e42caf77b 100644 --- a/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json +++ b/Application/EdFi.Ods.AdminApi.DBTests/appsettings.json @@ -10,5 +10,19 @@ "ConnectionStrings": { "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + }, + "tenant2": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + } } } diff --git a/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json b/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json new file mode 100644 index 000000000..a32758504 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.DBTests/testsappsettings.json @@ -0,0 +1,20 @@ +{ + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + }, + "tenant2": { + "ConnectionStrings": { + "EdFi_Admin": "Data Source=localhost;Initial Catalog=EdFi_Admin_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True", + "EdFi_Security": "Data Source=localhost;Initial Catalog=EdFi_Security_Test;Integrated Security=False;User Id=sa;Password=P@55w0rd;Encrypt=true;Trusted_Connection=false;TrustServerCertificate=True" + } + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj b/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj index 6363b7252..35363105e 100644 --- a/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj +++ b/Application/EdFi.Ods.AdminApi.UnitTests/EdFi.Ods.AdminApi.UnitTests.csproj @@ -15,7 +15,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs new file mode 100644 index 000000000..db9aeb241 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantTests.cs @@ -0,0 +1,111 @@ +// 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 AutoMapper; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class AddTenantTests +{ + [Test] + public async Task Handle_ReturnsBadRequest_WhenNotMultiTenant() + { + var options = A.Fake>(); + A.CallTo(() => options.Value).Returns(new AppSettings { MultiTenancy = false, DatabaseEngine = "Postgres" }); + + var addTenantCommand = new TestAddTenantCommand(); + var mapper = A.Fake(); + var request = new AddTenant.AddTenantRequest { TenantName = "tenant1" }; + var serviceScopeFactory = A.Fake(); + var tenantsService = A.Fake(); + var scope = A.Fake(); + var scopedServiceProvider = A.Fake(); + + A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(tenantsService); + + A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(scope); + A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); + + var validator = new TestValidator(tenantsService, options); + + var result = await AddTenant.Handle(validator, addTenantCommand, mapper, request, serviceScopeFactory, options); + + // Assert + var badRequest = result as IResult; + badRequest.ShouldNotBeNull(); + badRequest.GetType().Name.ShouldStartWith("BadRequest"); + var valueProperty = badRequest.GetType().GetProperty("Value"); + valueProperty.ShouldNotBeNull(); + var value = valueProperty.GetValue(badRequest); + value.ShouldNotBeNull(); + value.ToString().ShouldContain("Not multitenant environment."); + } + + [Test] + public async Task Handle_CallsValidatorAndCommand_AndReturnsCreated_WhenValid() + { + var options = A.Fake>(); + A.CallTo(() => options.Value).Returns(new AppSettings { MultiTenancy = true, DatabaseEngine = "Postgres" }); + + var addTenantCommand = new TestAddTenantCommand(); + var mapper = A.Fake(); + var model = A.Fake(); + var request = new AddTenant.AddTenantRequest { TenantName = "tenant2" }; + var serviceScopeFactory = A.Fake(); + var tenantsService = A.Fake(); + var scope = A.Fake(); + var scopedServiceProvider = A.Fake(); + + A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(tenantsService); + A.CallTo(() => mapper.Map(request)).Returns(model); + + A.CallTo(() => serviceScopeFactory.CreateScope()).Returns(scope); + A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); + + var validator = new TestValidator(tenantsService, options); + + var result = await AddTenant.Handle(validator, addTenantCommand, mapper, request, serviceScopeFactory, options); + + result.ShouldNotBeNull(); + result.GetType().Name.ShouldBe("Created"); + } + + private class TestValidator(ITenantsService service, IOptions options) : AddTenant.Validator(service, options) + { + public Task GuardAsync(AddTenant.AddTenantRequest request) + { + // Always succeed + return Task.CompletedTask; + } + } + + private class TestAddTenantCommand : AddTenantCommand + { + public string ExecutedTenantName { get; private set; } + + public TestAddTenantCommand() : base(A.Fake()) + { + } + + public override void Execute(IAddTenantModel model) + { + ExecutedTenantName = model.TenantName; + } + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs new file mode 100644 index 000000000..4252d3eb4 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/AddTenantValidatorTests.cs @@ -0,0 +1,129 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class AddTenantValidatorTests +{ + private AddTenant.Validator _validator; + private ITenantsService _tenantsService; + private IOptions _options; + + [SetUp] + public void SetUp() + { + _tenantsService = A.Fake(); + _options = Options.Create(new AppSettings { DatabaseEngine = "SqlServer" }); + _validator = new AddTenant.Validator(_tenantsService, _options); + } + + [Test] + public void Should_Have_Error_When_TenantName_Is_Empty() + { + var model = new AddTenant.AddTenantRequest { TenantName = string.Empty }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_TenantName_Exceeds_Max_Length() + { + var model = new AddTenant.AddTenantRequest { TenantName = new string('A', 101) }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); + } + + [Test] + public async Task Should_Have_Error_When_TenantName_Is_Not_Unique() + { + var existingTenants = new List + { + new() { TenantName = "ExistingTenant" } + }; + A.CallTo(() => _tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(existingTenants)); + + var model = new AddTenant.AddTenantRequest { TenantName = "ExistingTenant" }; + var result = await _validator.ValidateAsync(model); + result.Errors.Any(x => x.PropertyName == nameof(model.TenantName)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_EdFiAdminConnectionString_Exceeds_Max_Length() + { + var model = new AddTenant.AddTenantRequest + { + TenantName = "UniqueTenant", + EdFiAdminConnectionString = new string('C', 501) + }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.EdFiAdminConnectionString)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_EdFiAdminConnectionString_Is_Invalid() + { + var model = new AddTenant.AddTenantRequest + { + TenantName = "UniqueTenant", + EdFiAdminConnectionString = "invalid-connection-string" + }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.EdFiAdminConnectionString)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_EdFiSecurityConnectionString_Exceeds_Max_Length() + { + var model = new AddTenant.AddTenantRequest + { + TenantName = "UniqueTenant", + EdFiSecurityConnectionString = new string('C', 501) + }; + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.EdFiSecurityConnectionString)).ShouldBeTrue(); + } + + [Test] + public void Should_Have_Error_When_EdFiSecurityConnectionString_Is_Invalid() + { + var model = new AddTenant.AddTenantRequest + { + TenantName = "UniqueTenant", + EdFiSecurityConnectionString = "invalid-connection-string" + }; + + var result = _validator.Validate(model); + result.Errors.Any(x => x.PropertyName == nameof(model.EdFiSecurityConnectionString)).ShouldBeTrue(); + } + + [Test] + public async Task Should_Not_Have_Error_For_Valid_Model() + { + // Setup empty tenants list to ensure uniqueness check passes + A.CallTo(() => _tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(new List())); + + var model = new AddTenant.AddTenantRequest + { + TenantName = "UniqueTenant", + EdFiAdminConnectionString = "Server=.;Database=Admin;Trusted_Connection=True;", + EdFiSecurityConnectionString = "Server=.;Database=Security;Trusted_Connection=True;" + }; + + var result = await _validator.ValidateAsync(model); + result.IsValid.ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs new file mode 100644 index 000000000..4173a0bcd --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/DeleteTenantTests.cs @@ -0,0 +1,153 @@ +// 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.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class DeleteTenantTests +{ + private ITenantsService _tenantsService = null!; + private DeleteTenantCommand _deleteTenantCommand = null!; + private IServiceScopeFactory _serviceScopeFactory = null!; + private IOptions _options = null!; + + [SetUp] + public void SetUp() + { + _tenantsService = A.Fake(); + _deleteTenantCommand = A.Fake(); + _serviceScopeFactory = A.Fake(); + _options = Options.Create(new AppSettings { MultiTenancy = true }); + } + + [Test] + public async Task Handle_ShouldReturnBadRequest_WhenMultiTenancyIsDisabled() + { + // Arrange + var options = Options.Create(new AppSettings { MultiTenancy = false }); + + // Act + var result = await DeleteTenant.Handle( + _tenantsService, + _deleteTenantCommand, + _serviceScopeFactory, + options, + "tenant1"); + + // Assert + var badRequest = result as IResult; + badRequest.ShouldNotBeNull(); + + badRequest.GetType().Name.ShouldStartWith("BadRequest"); + var valueProperty = badRequest.GetType().GetProperty("Value"); + valueProperty.ShouldNotBeNull(); + var value = valueProperty.GetValue(badRequest); + value.ShouldNotBeNull(); + value.ToString().ShouldContain("Not multitenant environment."); + } + + [Test] + public async Task Handle_ShouldReturnNotFound_WhenTenantDoesNotExist() + { + // Arrange + A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync(A._)).Returns(Task.FromResult(null)); + + // Act + var result = await DeleteTenant.Handle( + _tenantsService, + _deleteTenantCommand, + _serviceScopeFactory, + _options, + "tenant1"); + + // Assert + var notFound = result as IResult; + notFound.ShouldNotBeNull(); + var httpResult = notFound as Microsoft.AspNetCore.Http.HttpResults.NotFound; + httpResult.ShouldNotBeNull(); + } + + [Test] + public async Task Handle_ShouldDeleteTenantAndReturnOk_WhenTenantExists() + { + // Arrange + var tenant = new TenantModel { TenantName = "tenant1" }; + A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync("tenant1")).Returns(Task.FromResult(tenant)); + + var scope = A.Fake(); + var scopedServiceProvider = A.Fake(); + var scopedTenantsService = A.Fake(); + + A.CallTo(() => _serviceScopeFactory.CreateScope()).Returns(scope); + A.CallTo(() => scope.ServiceProvider).Returns(scopedServiceProvider); + A.CallTo(() => scopedServiceProvider.GetService(typeof(ITenantsService))).Returns(scopedTenantsService); + A.CallTo(() => scopedTenantsService.InitializeTenantsAsync()).Returns(Task.CompletedTask); + + // Use a test double for DeleteTenantCommand + var testDeleteTenantCommand = new TestDeleteTenantCommand(); + + // Act + var result = await DeleteTenant.Handle( + _tenantsService, + testDeleteTenantCommand, + _serviceScopeFactory, + _options, + "tenant1"); + + // Assert + testDeleteTenantCommand.ExecutedTenantName.ShouldBe("tenant1"); + A.CallTo(() => scopedTenantsService.InitializeTenantsAsync()).MustHaveHappened(); + + var okResult = result as IResult; + okResult.ShouldNotBeNull(); + var httpResult = okResult as Microsoft.AspNetCore.Http.HttpResults.Ok; + httpResult.ShouldNotBeNull(); + } + + [Test] + public void Handle_ShouldThrow_WhenGetTenantByTenantIdAsyncThrows() + { + // Arrange + A.CallTo(() => _tenantsService.GetTenantByTenantIdAsync(A._)).Throws(); + + // Act & Assert + Should.ThrowAsync(async () => + await DeleteTenant.Handle( + _tenantsService, + _deleteTenantCommand, + _serviceScopeFactory, + _options, + "tenant1")); + } + + private class TestDeleteTenantCommand : DeleteTenantCommand + { + public string ExecutedTenantName { get; private set; } + + public TestDeleteTenantCommand() : base(A.Fake()) + { + } + + public override void Execute(string tenantName) + { + ExecutedTenantName = tenantName; + } + } +} + diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs new file mode 100644 index 000000000..6a5a9b086 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/ReadTenantsTest.cs @@ -0,0 +1,131 @@ +// 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.Collections.Generic; +using System.Threading.Tasks; +using EdFi.Common.Configuration; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class ReadTenantsTest +{ + [Test] + public async Task GetTenantsByTenantIdAsync_ReturnsOk_WhenTenantExists() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "tenant1"; + + var tenant = new TenantModel + { + TenantName = tenantName, + ConnectionStrings = new TenantModelConnectionStrings + { + EdFiAdminConnectionString = "Host=adminhost;Database=admindb;", + EdFiSecurityConnectionString = "Host=sechost;Database=secdb;" + } + }; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantByTenantIdAsync(tenantName)).Returns(tenant); + + var result = await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + + result.ShouldNotBeNull(); + var okResult = result as IResult; + okResult.ShouldNotBeNull(); + } + + [Test] + public async Task GetTenantsByTenantIdAsync_ReturnsNotFound_WhenTenantDoesNotExist() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "missingTenant"; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantByTenantIdAsync(tenantName)).Returns((TenantModel)null); + + var result = await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + + result.ShouldNotBeNull(); + var notFoundResult = result as IResult; + notFoundResult.ShouldNotBeNull(); + } + + [Test] + public async Task GetTenantsAsync_ReturnsOk_WithTenantList() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var databaseEngine = DatabaseEngine.Postgres; + + var tenants = new List + { + new() { + TenantName = "tenant1", + ConnectionStrings = new TenantModelConnectionStrings + { + EdFiAdminConnectionString = "Host=adminhost;Database=admindb;", + EdFiSecurityConnectionString = "Host=sechost;Database=secdb;" + } + } + }; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = "Postgres" }); + A.CallTo(() => tenantsService.GetTenantsAsync(true)).Returns(Task.FromResult(tenants)); + + var result = await ReadTenants.GetTenantsAsync(tenantsService, memoryCache, options); + + result.ShouldNotBeNull(); + var okResult = result as IResult; + okResult.ShouldNotBeNull(); + } + + [Test] + public void GetTenantsByTenantIdAsync_ThrowsNotFoundException_WhenDatabaseEngineIsNull() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + var tenantName = "tenant1"; + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = null }); + + Should.ThrowAsync>(async () => + { + await ReadTenants.GetTenantsByTenantIdAsync(tenantsService, memoryCache, tenantName, options); + }); + } + + [Test] + public void GetTenantsAsync_ThrowsNotFoundException_WhenDatabaseEngineIsNull() + { + var tenantsService = A.Fake(); + var memoryCache = A.Fake(); + var options = A.Fake>(); + + A.CallTo(() => options.Value).Returns(new AppSettings { DatabaseEngine = null }); + + Should.ThrowAsync>(async () => + { + await ReadTenants.GetTenantsAsync(tenantsService, memoryCache, options); + }); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs new file mode 100644 index 000000000..eb0e940fe --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Features/Tenants/TenantModelTests.cs @@ -0,0 +1,56 @@ +// 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.Features.Tenants; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Features.Tenants; + +[TestFixture] +public class TenantModelTests +{ + [Test] + public void DefaultConstructor_ShouldInitializePropertiesToEmptyStrings() + { + // Act + var connectionStrings = new TenantModelConnectionStrings(); + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe(string.Empty); + connectionStrings.EdFiSecurityConnectionString.ShouldBe(string.Empty); + } + + [Test] + public void ParameterizedConstructor_ShouldSetProperties() + { + // Arrange + var adminConn = "AdminConn"; + var securityConn = "SecurityConn"; + + // Act + var connectionStrings = new TenantModelConnectionStrings(adminConn, securityConn); + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe(adminConn); + connectionStrings.EdFiSecurityConnectionString.ShouldBe(securityConn); + } + + [Test] + public void Properties_ShouldBeSettable() + { + // Arrange + var connectionStrings = new TenantModelConnectionStrings + { + // Act + EdFiAdminConnectionString = "NewAdmin", + EdFiSecurityConnectionString = "NewSecurity" + }; + + // Assert + connectionStrings.EdFiAdminConnectionString.ShouldBe("NewAdmin"); + connectionStrings.EdFiSecurityConnectionString.ShouldBe("NewSecurity"); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs new file mode 100644 index 000000000..43adf7c76 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/AddTenantCommandTests.cs @@ -0,0 +1,142 @@ +// 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.Text.Json.Nodes; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using FakeItEasy; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; + +[TestFixture] +public class AddTenantCommandTests +{ + private StubFileSystemAppSettingsFileProvider _fileProvider = null!; + private AddTenantCommand _command = null!; + + [SetUp] + public void SetUp() + { + _fileProvider = new StubFileSystemAppSettingsFileProvider(); + _command = new AddTenantCommand(_fileProvider); + } + + private class StubFileSystemAppSettingsFileProvider : FileSystemAppSettingsFileProvider, IAppSettingsFileProvider + { + public StubFileSystemAppSettingsFileProvider() : base("defaultFilePath") + { + } + + public string ReadText { get; set; } + public string WrittenText { get; private set; } + + public new string ReadAllText() + { + return ReadText ?? string.Empty; + } + + public new void WriteAllText(string text) + { + WrittenText = text; + } + } + + // Update tests to use the stub + [Test] + public void Execute_ShouldAddTenant_WhenTenantDoesNotExist() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ + ""Tenants"": { + ""existingTenant"": { + ""ConnectionStrings"": { + ""EdFi_Security"": ""sec"", + ""EdFi_Admin"": ""admin"" + } + } + } + }"; + + var model = A.Fake(); + A.CallTo(() => model.TenantName).Returns("newTenant"); + A.CallTo(() => model.EdFiSecurityConnectionString).Returns("sec2"); + A.CallTo(() => model.EdFiAdminConnectionString).Returns("admin2"); + + // Act + _command.Execute(model); + + // Assert + stubProvider.WrittenText.ShouldNotBeNull(); + var root = JsonNode.Parse(stubProvider.WrittenText!)!; + var tenants = root["Tenants"]!.AsObject(); + tenants.ContainsKey("newTenant").ShouldBeTrue(); + tenants["newTenant"]!["ConnectionStrings"]!["EdFi_Security"]!.ToString().ShouldBe("sec2"); + tenants["newTenant"]!["ConnectionStrings"]!["EdFi_Admin"]!.ToString().ShouldBe("admin2"); + } + + [Test] + public void Execute_ShouldThrow_WhenTenantAlreadyExists() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ + ""Tenants"": { + ""existingTenant"": { + ""ConnectionStrings"": { + ""EdFi_Security"": ""sec"", + ""EdFi_Admin"": ""admin"" + } + } + } + }"; + + var model = A.Fake(); + A.CallTo(() => model.TenantName).Returns("existingTenant"); + A.CallTo(() => model.EdFiSecurityConnectionString).Returns("sec"); + A.CallTo(() => model.EdFiAdminConnectionString).Returns("admin"); + + // Act & Assert + var ex = Should.Throw(() => _command.Execute(model)); + ex.Message.ShouldContain("already exists"); + } + + [Test] + public void Execute_ShouldThrow_WhenTenantsSectionMissing() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ ""SomeOtherSection"": {} }"; + + var model = A.Fake(); + A.CallTo(() => model.TenantName).Returns("tenantX"); + A.CallTo(() => model.EdFiSecurityConnectionString).Returns("secX"); + A.CallTo(() => model.EdFiAdminConnectionString).Returns("adminX"); + + // Act & Assert + var ex = Should.Throw(() => _command.Execute(model)); + ex.Message.ShouldContain("Tenants section missing"); + } + + [Test] + public void Execute_ShouldThrow_WhenAppSettingsIsEmptyOrInvalid() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = string.Empty; + + var model = A.Fake(); + A.CallTo(() => model.TenantName).Returns("tenantY"); + A.CallTo(() => model.EdFiSecurityConnectionString).Returns("secY"); + A.CallTo(() => model.EdFiAdminConnectionString).Returns("adminY"); + + // Act & Assert + var ex = Should.Throw(() => _command.Execute(model)); + ex.Message.ShouldContain("appsettings.json contains invalid JSON."); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs new file mode 100644 index 000000000..812140e0d --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Database/Commands/DeleteTenantCommandTests.cs @@ -0,0 +1,125 @@ +// 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.Text.Json.Nodes; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Database.Commands; + +[TestFixture] +public class DeleteTenantCommandTests +{ + private StubFileSystemAppSettingsFileProvider _fileProvider = null!; + private DeleteTenantCommand _command = null!; + + [SetUp] + public void SetUp() + { + _fileProvider = new StubFileSystemAppSettingsFileProvider(); + _command = new DeleteTenantCommand(_fileProvider); + } + + private class StubFileSystemAppSettingsFileProvider : FileSystemAppSettingsFileProvider, IAppSettingsFileProvider + { + public StubFileSystemAppSettingsFileProvider() : base("defaultFilePath") + { + } + + public string ReadText { get; set; } + public string WrittenText { get; private set; } + + public new string ReadAllText() + { + return ReadText ?? string.Empty; + } + + public new void WriteAllText(string text) + { + WrittenText = text; + } + } + + [Test] + public void Execute_ShouldRemoveTenant_WhenTenantExists() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ + ""Tenants"": { + ""tenant1"": { + ""ConnectionStrings"": { + ""EdFi_Security"": ""sec1"", + ""EdFi_Admin"": ""admin1"" + } + }, + ""tenant2"": { + ""ConnectionStrings"": { + ""EdFi_Security"": ""sec2"", + ""EdFi_Admin"": ""admin2"" + } + } + } + }"; + + // Act + _command.Execute("tenant1"); + + // Assert + stubProvider.WrittenText.ShouldNotBeNull(); + var root = JsonNode.Parse(stubProvider.WrittenText!)!; + var tenants = root["Tenants"]!.AsObject(); + tenants.ContainsKey("tenant1").ShouldBeFalse(); + tenants.ContainsKey("tenant2").ShouldBeTrue(); + } + + [Test] + public void Execute_ShouldThrow_WhenTenantDoesNotExist() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ + ""Tenants"": { + ""tenant2"": { + ""ConnectionStrings"": { + ""EdFi_Security"": ""sec2"", + ""EdFi_Admin"": ""admin2"" + } + } + } + }"; + + // Act & Assert + var ex = Should.Throw(() => _command.Execute("tenant1")); + ex.Message.ShouldContain("does not exist"); + } + + [Test] + public void Execute_ShouldThrow_WhenTenantsSectionMissing() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = @"{ ""SomeOtherSection"": {} }"; + + // Act & Assert + var ex = Should.Throw(() => _command.Execute("tenant1")); + ex.Message.ShouldContain("Tenants section missing"); + } + + [Test] + public void Execute_ShouldThrow_WhenAppSettingsIsEmptyOrInvalid() + { + // Arrange + var stubProvider = _fileProvider; + stubProvider.ReadText = string.Empty; + + // Act & Assert + var ex = Should.Throw(() => _command.Execute("tenant1")); + ex.Message.ShouldContain("appsettings.json contains invalid JSON."); + } +} diff --git a/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs new file mode 100644 index 000000000..d83fc139c --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.UnitTests/Infrastructure/Services/Tenants/TenantServiceTests.cs @@ -0,0 +1,158 @@ +// 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.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using EdFi.Ods.AdminApi.Common.Constants; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FakeItEasy; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Shouldly; + +namespace EdFi.Ods.AdminApi.UnitTests.Infrastructure.Services.Tenants; + +[TestFixture] +internal class TenantServiceTests +{ + private IOptionsSnapshot _options = null!; + private IMemoryCache _memoryCache = null!; + private AppSettingsFile _appSettings = null!; + + [SetUp] + public void SetUp() + { + _options = A.Fake>(); + _memoryCache = new MemoryCache(new MemoryCacheOptions()); + _appSettings = new AppSettingsFile + { + AppSettings = new AppSettings + { + MultiTenancy = true, + DatabaseEngine = "SqlServer" + }, + Tenants = new Dictionary + { + { + "tenantA", new TenantSettings + { + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-A" }, + { "EdFi_Security", "security-conn-A" } + } + } + }, + { + "tenantB", new TenantSettings + { + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-B" }, + { "EdFi_Security", "security-conn-B" } + } + } + } + }, + ConnectionStrings = new Dictionary + { + { "EdFi_Admin", "admin-conn-default" }, + { "EdFi_Security", "security-conn-default" } + }, + SwaggerSettings = new(), + Testing = new() + }; + + A.CallTo(() => _options.Value).Returns(_appSettings); + } + + [TearDown] + public void TearDown() + { + _memoryCache.Dispose(); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_All_Tenants_When_MultiTenancy_Enabled() + { + var service = new TenantService(_options, _memoryCache); + + var tenants = await service.GetTenantsAsync(); + + tenants.Count.ShouldBe(2); + tenants.Any(t => t.TenantName == "tenantA").ShouldBeTrue(); + tenants.Any(t => t.TenantName == "tenantB").ShouldBeTrue(); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_DefaultTenant_When_MultiTenancy_Disabled() + { + _appSettings.AppSettings.MultiTenancy = false; + var service = new TenantService(_options, _memoryCache); + + var tenants = await service.GetTenantsAsync(); + + tenants.Count.ShouldBe(1); + tenants[0].TenantName.ShouldBe(Constants.DefaultTenantName); + tenants[0].ConnectionStrings.EdFiAdminConnectionString.ShouldBe("admin-conn-default"); + tenants[0].ConnectionStrings.EdFiSecurityConnectionString.ShouldBe("security-conn-default"); + } + + [Test] + public async Task GetTenantByTenantIdAsync_Should_Return_Correct_Tenant() + { + var service = new TenantService(_options, _memoryCache); + + var tenant = await service.GetTenantByTenantIdAsync("tenantA"); + + tenant.ShouldNotBeNull(); + tenant!.TenantName.ShouldBe("tenantA"); + tenant.ConnectionStrings.EdFiAdminConnectionString.ShouldBe("admin-conn-A"); + tenant.ConnectionStrings.EdFiSecurityConnectionString.ShouldBe("security-conn-A"); + } + + [Test] + public async Task GetTenantByTenantIdAsync_Should_Return_Null_If_Not_Found() + { + var service = new TenantService(_options, _memoryCache); + + var tenant = await service.GetTenantByTenantIdAsync("notfound"); + + tenant.ShouldBeNull(); + } + + [Test] + public async Task InitializeTenantsAsync_Should_Store_Tenants_In_Cache() + { + var service = new TenantService(_options, _memoryCache); + + await service.InitializeTenantsAsync(); + + var cached = _memoryCache.Get>(Constants.TenantsCacheKey); + cached.ShouldNotBeNull(); + cached!.Count.ShouldBe(2); + } + + [Test] + public async Task GetTenantsAsync_Should_Return_From_Cache_If_Requested() + { + var service = new TenantService(_options, _memoryCache); + + // Prime the cache + await service.InitializeTenantsAsync(); + + // Remove a tenant from the underlying settings to prove cache is used + _appSettings.Tenants.Remove("tenantA"); + + var tenants = await service.GetTenantsAsync(fromCache: true); + + tenants.Count.ShouldBe(2); + tenants.Any(t => t.TenantName == "tenantA").ShouldBeTrue(); + } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs new file mode 100644 index 000000000..8c23db51b --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/ReadTenants.cs @@ -0,0 +1,71 @@ +// 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.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.V1.Features.Tenants; + +public class ReadTenants : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/tenants", GetTenantsAsync) + .BuildForVersions(AdminApiVersions.V1); + } + + public static IResult GetTenantsAsync(IOptions options, IOptions _appSettings) + { + const string ADMIN_DB_KEY = "EdFi_Admin"; + const string SECURITY_DB_KEY = "EdFi_Security"; + var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var defaultTenant = new TenantModel() + { + TenantName = Common.Constants.Constants.DefaultTenantName, + ConnectionStrings = new TenantModelConnectionStrings + ( + edFiAdminConnectionString: _appSettings.Value.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value, + edFiSecurityConnectionString: _appSettings.Value.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value + ) + }; + + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, defaultTenant.ConnectionStrings.EdFiAdminConnectionString); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, defaultTenant.ConnectionStrings.EdFiSecurityConnectionString); + + var response = new TenantsResponse + { + TenantName = defaultTenant.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }; + return Results.Ok(response); + } +} + +public class TenantsResponse +{ + public string? TenantName { get; set; } + public EdfiConnectionString? AdminConnectionString { get; set; } + public EdfiConnectionString? SecurityConnectionString { get; set; } +} + +public class EdfiConnectionString +{ + public string? host { get; set; } + public string? database { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs new file mode 100644 index 000000000..90394a0eb --- /dev/null +++ b/Application/EdFi.Ods.AdminApi.V1/Features/Tenants/TenantModel.cs @@ -0,0 +1,38 @@ +// 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.Common.Constants; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.V1.Features.Tenants; + +[SwaggerSchema] +public class TenantModel +{ + [SwaggerSchema(Description = Constants.TenantNameDescription, Nullable = false)] + public required string TenantName { get; set; } + + [SwaggerSchema(Description = Constants.TenantConnectionStringDescription, Nullable = false)] + public TenantModelConnectionStrings ConnectionStrings { get; set; } = new(); +} + +[SwaggerSchema] +public class TenantModelConnectionStrings +{ + public string EdFiSecurityConnectionString { get; set; } + public string EdFiAdminConnectionString { get; set; } + + public TenantModelConnectionStrings() + { + EdFiAdminConnectionString = string.Empty; + EdFiSecurityConnectionString = string.Empty; + } + + public TenantModelConnectionStrings(string edFiAdminConnectionString, string edFiSecurityConnectionString) + { + EdFiAdminConnectionString = edFiAdminConnectionString; + EdFiSecurityConnectionString = edFiSecurityConnectionString; + } +} diff --git a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleBuilderExtension.cs b/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleBuilderExtension.cs deleted file mode 100644 index c9327883a..000000000 --- a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleBuilderExtension.cs +++ /dev/null @@ -1,48 +0,0 @@ -// 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.Common.Constants; -using log4net; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; - -namespace EdFi.Ods.AdminApi.AdminConsole; - -public static class AdminConsoleBuilderExtension -{ - public static void RegisterAdminConsoleDependencies(this WebApplicationBuilder webApplicationBuilder) - { - webApplicationBuilder.AddAdminConsoleServices(); - } - - public static void RegisterAdminConsoleCorsDependencies(this WebApplicationBuilder webApplicationBuilder, ILog logger) - { - var corsSettings = webApplicationBuilder.Configuration.GetSection(AdminConsoleConstants.AdminConsoleSettingsKey); - var enableCors = corsSettings.GetValue(AdminConsoleConstants.EnableCorsKey); - var allowedOrigins = corsSettings.GetSection(AdminConsoleConstants.AllowedOriginsCorsKey).Get(); - // Read CORS settings from configuration - if (enableCors && allowedOrigins != null) - { - if (allowedOrigins.Length > 0) - { - webApplicationBuilder.Services.AddCors(options => - { - options.AddPolicy(AdminConsoleConstants.CorsPolicyName, policy => - { - policy.WithOrigins(allowedOrigins) - .AllowAnyMethod() - .AllowAnyHeader(); - }); - }); - } - else - { - // Handle the case where allowedOrigins is null or empty - logger.Warn("CORS is enabled, but no allowed origins are specified."); - } - } - } -} diff --git a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleExtension.cs b/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleExtension.cs deleted file mode 100644 index ba0308c2b..000000000 --- a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/AdminConsoleExtension.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.Services.Tenants; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Context; -using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; -using EdFi.Ods.AdminApi.Common.Settings; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.AdminConsole.Configurations; - -public static class AdminConsoleExtension -{ - public static void UseCorsForAdminConsole(this WebApplication app) - { - var adminConsoleSettings = app.Services.GetService>(); - - if (adminConsoleSettings != null && adminConsoleSettings.Value.CorsSettings.EnableCors) - app.UseCors(AdminConsoleConstants.CorsPolicyName); - } -} diff --git a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/ServicesBuilderExtension.cs b/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/ServicesBuilderExtension.cs deleted file mode 100644 index 2f2268919..000000000 --- a/Application/EdFi.Ods.AdminApi/AdminConsole/Configurations/ServicesBuilderExtension.cs +++ /dev/null @@ -1,69 +0,0 @@ -// 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; -using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services; -using EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; -using EdFi.Ods.AdminApi.Common.Settings; -using EdFi.Ods.AdminApi.Infrastructure.Database.Queries; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.AdminConsole; - -public static class ServicesBuilderExtension -{ - public static void AddAdminConsoleServices(this WebApplicationBuilder builder) - { - builder.Services.AddHostedService(); - - builder.Services.Configure(builder.Configuration); - - builder.Services.Configure(builder.Configuration.GetSection("AdminConsoleSettings")); - - builder.Services.AddTransient(); - - builder.RegisterAdminConsoleServices(); - - } - - private static void RegisterAdminConsoleServices(this WebApplicationBuilder builder) - { - foreach (var type in typeof(IMarkerForEdFiAdminConsoleManagement).Assembly.GetTypes()) - { - if (type.IsClass && !type.IsAbstract && (type.IsPublic || type.IsNestedPublic)) - { - var concreteClass = type; - - var interfaces = concreteClass.GetInterfaces().ToArray(); - - if (concreteClass.Namespace != null) - { - if (!concreteClass.Namespace.EndsWith("Commands") && - !concreteClass.Namespace.EndsWith("Queries")) - { - continue; - } - - if (interfaces.Length == 1) - { - var serviceType = interfaces.Single(); - if (serviceType.FullName == $"{concreteClass.Namespace}.I{concreteClass.Name}") - builder.Services.AddScoped(serviceType, concreteClass); - } - else if (interfaces.Length == 0) - { - if (!concreteClass.Name.EndsWith("Command") - && !concreteClass.Name.EndsWith("Query") - && !concreteClass.Name.EndsWith("Service")) - { - continue; - } - builder.Services.AddScoped(concreteClass); - } - } - } - } - } -} diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json index e0083d7cc..aa71790ce 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V1/Admin API E2E.postman_collection.json @@ -1,8 +1,8 @@ { "info": { - "_postman_id": "806b2570-328a-4f44-8095-746d06c2e024", + "_postman_id": "47e794c4-64a5-47f8-99b9-49d8f0358bbe", "name": "Admin API E2E refactor", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "22794466" }, "item": [ @@ -50,7 +50,12 @@ } } }, - "url": "{{API_URL}}" + "url": { + "raw": "{{API_URL}}", + "host": [ + "{{API_URL}}" + ] + } }, "response": [] } @@ -103,7 +108,16 @@ } } }, - "url": "{{API_URL}}/v2/vendors" + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } }, "response": [] }, @@ -165,7 +179,16 @@ } } }, - "url": "{{API_URL}}/v1/vendors" + "url": { + "raw": "{{API_URL}}/v1/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors" + ] + } }, "response": [ { @@ -182,7 +205,15 @@ } } }, - "url": "{{API_URL}}/vendors" + "url": { + "raw": "{{API_URL}}/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "vendors" + ] + } }, "status": "Created", "code": 201, @@ -260,7 +291,16 @@ } } }, - "url": "{{API_URL}}/v1/vendors" + "url": { + "raw": "{{API_URL}}/v1/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors" + ] + } }, "response": [] }, @@ -318,7 +358,16 @@ } } }, - "url": "{{API_URL}}/v1/vendors" + "url": { + "raw": "{{API_URL}}/v1/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors" + ] + } }, "response": [] }, @@ -363,7 +412,16 @@ } } }, - "url": "{{API_URL}}/v2/vendors" + "url": { + "raw": "{{API_URL}}/v2/vendors", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors" + ] + } }, "response": [] }, @@ -405,7 +463,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -467,7 +535,17 @@ } } }, - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -519,7 +597,17 @@ } } }, - "url": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -579,7 +667,17 @@ } } }, - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -612,7 +710,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v2/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -647,7 +755,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -685,7 +803,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -732,7 +860,17 @@ } } }, - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] }, @@ -772,16 +910,30 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{CreatedVendorId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{CreatedVendorId}}" + ] + } }, "response": [] } ], "auth": { "type": "bearer", - "bearer": { - "token": "{{TOKEN}}" - } + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] }, "event": [ { @@ -909,7 +1061,17 @@ } } }, - "url": "{{API_URL}}/v2/applications/" + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } }, "response": [] }, @@ -1033,7 +1195,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/" + "url": { + "raw": "{{API_URL}}/v1/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "" + ] + } }, "response": [] }, @@ -1093,7 +1265,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/" + "url": { + "raw": "{{API_URL}}/v1/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "" + ] + } }, "response": [] }, @@ -1152,7 +1334,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/" + "url": { + "raw": "{{API_URL}}/v1/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "" + ] + } }, "response": [] }, @@ -1212,7 +1404,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/" + "url": { + "raw": "{{API_URL}}/v1/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "" + ] + } }, "response": [] }, @@ -1272,7 +1474,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/applications/" + "url": { + "raw": "{{API_URL}}/v1/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "" + ] + } }, "response": [] }, @@ -1305,7 +1517,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v2/applications/" + "url": { + "raw": "{{API_URL}}/v2/applications/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "" + ] + } }, "response": [] }, @@ -1359,7 +1581,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1487,7 +1719,18 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/vendors/{{ApplicationVendorId}}/applications" + "url": { + "raw": "{{API_URL}}/v1/vendors/{{ApplicationVendorId}}/applications", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "vendors", + "{{ApplicationVendorId}}", + "applications" + ] + } }, "response": [] }, @@ -1582,7 +1825,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1634,7 +1887,17 @@ } } }, - "url": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1694,7 +1957,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1753,7 +2026,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1813,7 +2096,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1856,7 +2149,18 @@ "request": { "method": "PUT", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}/reset-credential" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } }, "response": [] }, @@ -1889,7 +2193,18 @@ "request": { "method": "PUT", "header": [], - "url": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential" + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } }, "response": [] }, @@ -1922,7 +2237,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v2/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1956,7 +2281,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -1994,7 +2329,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -2032,7 +2377,18 @@ "request": { "method": "PUT", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}/reset-credential" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}/reset-credential", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}", + "reset-credential" + ] + } }, "response": [] }, @@ -2079,7 +2435,17 @@ } } }, - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] }, @@ -2149,16 +2515,30 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}" + "url": { + "raw": "{{API_URL}}/v1/applications/{{CreatedApplicationId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "applications", + "{{CreatedApplicationId}}" + ] + } }, "response": [] } ], "auth": { "type": "bearer", - "bearer": { - "token": "{{TOKEN}}" - } + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] }, "event": [ { @@ -2234,7 +2614,17 @@ } } }, - "url": "{{API_URL}}/v2/claimsets/" + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2302,7 +2692,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2371,7 +2771,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2429,7 +2839,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2487,7 +2907,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2568,7 +2998,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2636,7 +3076,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2701,7 +3151,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2762,7 +3222,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2824,7 +3294,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2897,7 +3377,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/claimsets/" + "url": { + "raw": "{{API_URL}}/v1/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2930,7 +3420,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v2/claimsets/" + "url": { + "raw": "{{API_URL}}/v2/claimsets/", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "" + ] + } }, "response": [] }, @@ -2976,7 +3476,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3025,7 +3535,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetIdOverride}}" + ] + } }, "response": [] }, @@ -3092,7 +3612,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3144,7 +3674,17 @@ } } }, - "url": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3217,7 +3757,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetIdOverride}}" + ] + } }, "response": [] }, @@ -3274,7 +3824,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3331,7 +3891,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3389,7 +3959,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3453,7 +4033,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3517,7 +4107,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3578,7 +4178,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3639,7 +4249,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3699,7 +4319,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{SystemReservedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } }, "response": [] }, @@ -3732,7 +4362,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v2/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3767,7 +4407,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -3802,7 +4452,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetIdOverride}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetIdOverride}}" + ] + } }, "response": [] }, @@ -3856,7 +4516,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{SystemReservedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{SystemReservedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{SystemReservedClaimSetId}}" + ] + } }, "response": [] }, @@ -3925,7 +4595,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{OtherExistingClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{OtherExistingClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{OtherExistingClaimSetId}}" + ] + } }, "response": [] }, @@ -3963,7 +4643,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -4006,7 +4696,17 @@ } } }, - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] }, @@ -4058,16 +4758,30 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}" + "url": { + "raw": "{{API_URL}}/v1/claimsets/{{CreatedClaimSetId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "claimsets", + "{{CreatedClaimSetId}}" + ] + } }, "response": [] } ], "auth": { "type": "bearer", - "bearer": { - "token": "{{TOKEN}}" - } + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] }, "event": [ { @@ -4143,7 +4857,16 @@ } } }, - "url": "{{API_URL}}/v2/odsInstances" + "url": { + "raw": "{{API_URL}}/v2/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ] + } }, "response": [] }, @@ -4215,7 +4938,16 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances" + "url": { + "raw": "{{API_URL}}/v1/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances" + ] + } }, "response": [] }, @@ -4276,7 +5008,16 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances" + "url": { + "raw": "{{API_URL}}/v1/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances" + ] + } }, "response": [] }, @@ -4335,7 +5076,16 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances" + "url": { + "raw": "{{API_URL}}/v1/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances" + ] + } }, "response": [] }, @@ -4404,7 +5154,16 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances" + "url": { + "raw": "{{API_URL}}/v1/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances" + ] + } }, "response": [] }, @@ -4459,7 +5218,16 @@ } } }, - "url": "{{API_URL}}/v2/odsInstances" + "url": { + "raw": "{{API_URL}}/v2/odsInstances", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances" + ] + } }, "response": [] }, @@ -4502,7 +5270,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4566,7 +5344,17 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4618,7 +5406,17 @@ } } }, - "url": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4731,7 +5529,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4764,7 +5572,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v2/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4799,7 +5617,17 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4837,7 +5665,17 @@ "request": { "method": "GET", "header": [], - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4884,7 +5722,17 @@ } } }, - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] }, @@ -4924,16 +5772,30 @@ "request": { "method": "DELETE", "header": [], - "url": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}" + "url": { + "raw": "{{API_URL}}/v1/odsInstances/{{CreateOdsInstanceId}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "odsInstances", + "{{CreateOdsInstanceId}}" + ] + } }, "response": [] } ], "auth": { "type": "bearer", - "bearer": { - "token": "{{TOKEN}}" - } + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] }, "event": [ { @@ -4957,13 +5819,148 @@ } } ] + }, + { + "name": "Tenants", + "item": [ + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const response = pm.response.json();", + "const result = pm.response.json();", + "", + "pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " pm.expect(result.tenantName).to.equal(\"default\");", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + "});", + "", + "const GetTenantsSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + "}", + "", + "pm.test(\"GET Tenants: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + } + ] } ], "auth": { "type": "bearer", - "bearer": { - "token": "{{TOKEN}}" - } + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] }, "event": [ { diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json index f4d0500f3..811c6f7b8 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-mssql.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "securityconnectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Security;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, { "key": "isMultitenant", "value": "true", diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json index 3db46273a..723d046e6 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-multitenant-pgsql.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "securityconnectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, { "key": "isMultitenant", "value": "true", diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json index ea3e9244e..45a2ca205 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-mssql.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "securityconnectionString", + "value": "Data Source=db-admin;Initial Catalog=EdFi_Security;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true", + "type": "default", + "enabled": true + }, { "key": "isMultitenant", "value": "false", diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json index 90bb4af13..5aa1e0328 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API Docker-single-pgsql.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "securityconnectionString", + "value": "host=test;port=90;username=test;password=test;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, { "key": "isMultitenant", "value": "false", diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AdminConsole.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AdminConsole.postman_collection.json deleted file mode 100644 index 33d17670e..000000000 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - AdminConsole.postman_collection.json +++ /dev/null @@ -1,349 +0,0 @@ -{ - "info": { - "_postman_id": "bf0ae1a5-bb6e-4b20-9ae0-b4adc2e2df0b", - "name": "Admin API E2E 2.0 - AdminConsole", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "42411115", - "_collection_link": "https://dewfdaf.postman.co/workspace/ADMINAPI-1295~b8166cce-f66f-4e17-acd8-c8f40ac88d5e/collection/42411115-bf0ae1a5-bb6e-4b20-9ae0-b4adc2e2df0b?action=share&source=collection_link&creator=42411115" - }, - "item": [ - { - "name": "AdminConsole", - "item": [ - { - "name": "Tenants", - "item": [ - { - "name": "Tenants", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"GET Tenants: Status code is Found\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const GetTenantsSchema = {", - "", - " \"type\": \"array\",", - " \"items\": [", - " {", - " \"type\": \"object\",", - " \"properties\": {", - " \"tenantId\": {", - " \"type\": \"integer\"", - " },", - " \"document\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"edfiApiDiscoveryUrl\": {", - " \"type\": \"string\"", - " },", - " \"name\": {", - " \"type\": \"string\"", - " }", - " },", - " \"required\": [", - " \"edfiApiDiscoveryUrl\",", - " \"name\"", - " ]", - " }", - " },", - " \"required\": [", - " \"tenantId\",", - " \"document\"", - " ]", - " }", - " ]", - "}", - "", - "pm.test(\"GET Tenants: Validation Schema Response\", () => {", - " pm.response.to.have.jsonSchema(GetTenantsSchema);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{API_URL}}/adminconsole/tenants", - "host": [ - "{{API_URL}}" - ], - "path": [ - "adminconsole", - "tenants" - ] - } - }, - "response": [] - }, - { - "name": "Tenants Invalid", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"GET Tenants: Status code is Invalid\", function () {", - " pm.response.to.have.status(404);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{API_URL}}/adminconsole/tenant", - "host": [ - "{{API_URL}}" - ], - "path": [ - "adminconsole", - "tenant" - ] - } - }, - "response": [] - }, - { - "name": "Tenants by Tenant Id", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", - " pm.response.to.have.status(200);", - "});", - "", - "const GetTenantsByTenantSchema = {", - " \"type\": \"object\",", - " \"properties\": {", - " \"tenantId\": {", - " \"type\": \"integer\"", - " },", - " \"document\": {", - " \"type\": \"object\",", - " \"properties\": {", - " \"edfiApiDiscoveryUrl\": {", - " \"type\": \"string\"", - " },", - " \"name\": {", - " \"type\": \"string\"", - " }", - " },", - " \"required\": [", - " \"edfiApiDiscoveryUrl\",", - " \"name\"", - " ]", - " }", - " },", - " \"required\": [", - " \"tenantId\",", - " \"document\"", - " ]", - "}", - "", - "pm.test(\"GET Tenants by Tenant Id: Validation Schema Response\", () => {", - " pm.response.to.have.jsonSchema(GetTenantsByTenantSchema);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{API_URL}}/adminconsole/tenants/1", - "host": [ - "{{API_URL}}" - ], - "path": [ - "adminconsole", - "tenants", - "1" - ] - } - }, - "response": [] - }, - { - "name": "Tenants by Tenant Id Not Found", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"GET Tenants: Status code is Found\", function () {", - " pm.response.to.have.status(404);", - "});", - "" - ], - "type": "text/javascript", - "packages": {} - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{API_URL}}/adminconsole/tenants/100", - "host": [ - "{{API_URL}}" - ], - "path": [ - "adminconsole", - "tenants", - "100" - ] - } - }, - "response": [] - } - ] - } - ] - } - ], - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{TOKEN}}", - "type": "string" - } - ] - }, - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "function generateClientSecret() {", - " const minLength = 32;", - " const maxLength = 128;", - " let result = '';", - " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", - " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", - " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", - "", - " result += randomChar('abcdefghijklmnopqrstuvwxyz');", - " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", - " result += randomChar('0123456789');", - " result += randomChar(specialCharacters);", - "", - " for (let i = result.length; i < length; i++) {", - " const charactersPlusSpecial = characters + specialCharacters;", - " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", - " }", - "", - " return shuffleString(result);", - "}", - "", - "function randomChar(str) {", - " return str.charAt(Math.floor(Math.random() * str.length));", - "}", - "", - "function shuffleString(str) {", - " const array = str.split('');", - " for (let i = array.length - 1; i > 0; i--) {", - " const j = Math.floor(Math.random() * (i + 1));", - " [array[i], array[j]] = [array[j], array[i]];", - " }", - " return array.join('');", - "}", - "", - "let guid = pm.variables.replaceIn('{{$guid}}');", - "let client_secret = generateClientSecret();", - "", - "let header = {", - " 'Content-Type': 'application/x-www-form-urlencoded'", - "};", - "", - "if (pm.variables.get(\"isMultitenant\") == \"true\") {", - " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", - " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", - "}", - "", - "pm.sendRequest({", - " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", - " method: 'POST',", - " header: header,", - " body: {", - " mode: 'urlencoded',", - " urlencoded: [", - " {key: 'ClientId', value: guid },", - " {key: 'ClientSecret', value: client_secret },", - " {key: 'DisplayName', value: guid }", - " ]", - " }", - "},", - " (err, res) => {", - " error = res.json().error", - " if(error) {", - " throw res.json().error_description", - " }", - "", - "pm.sendRequest({", - " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", - " method: 'POST',", - " header: header,", - " body: {", - " mode: 'urlencoded',", - " urlencoded: [", - " {key: 'client_id', value: guid },", - " {key: 'client_secret', value: client_secret },", - " {key: 'grant_type', value: \"client_credentials\"},", - " {key: 'scope', value: \"edfi_admin_api/full_access\"}", - " ]", - " }", - "},", - " (err, res) => {", - " error = res.json().error", - " if(error) {", - " throw res.json().error_description", - " }", - " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", - "});", - "});" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "packages": {}, - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "key": "TOKEN", - "value": "" - } - ] -} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json new file mode 100644 index 000000000..89dfee891 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API E2E 2.0 - Tenants.postman_collection.json @@ -0,0 +1,985 @@ +{ + "info": { + "_postman_id": "d327aadc-5703-41da-9ee8-113331168025", + "name": "Admin API E2E 2.0 - Tenants", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "22794466" + }, + "item": [ + { + "name": "V2", + "item": [ + { + "name": "Tenants", + "item": [ + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"POST Tenants: Status code is Created\", function () {", + " pm.response.to.have.status(201);", + " });", + "}", + "else {", + " pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + " });", + "", + " pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"CreatedTenantName\", \"Tenant-\" + pm.variables.replaceIn('{{$guid}}'));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"TenantName\": \"{{CreatedTenantName}}\",\r\n \"EdFiSecurityConnectionString\": \"{{securityconnectionString}}\",\r\n \"EdFiAdminConnectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants Just Name", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"POST Tenants: Status code is Created\", function () {", + " pm.response.to.have.status(201);", + " });", + "}", + "else {", + " pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + " });", + "", + " pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "pm.collectionVariables.set(\"TenantGUID2\", pm.variables.replaceIn('{{$guid}}'));\r", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID2}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v1/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Admin connection string", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"title\");", + " pm.expect(response).to.have.property(\"errors\");", + " });", + "", + " pm.test(\"POST Tenants Invalid EdFiAdminConnectionString: Response title is helpful and accurate\", function () {", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");", + " });", + "", + " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages by property\", function () {", + " pm.expect(response.errors[\"EdFiAdminConnectionString\"].length).to.equal(1);", + " });", + "", + " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages with wrong elements\", function () {", + " pm.expect(response.errors[\"EdFiAdminConnectionString\"][0]).to.contain(\"is not valid\");", + " });", + "}", + "else {", + " pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + " });", + "", + " pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\",\r\n \"EdFiSecurityConnectionString\": \"{{securityconnectionString}}\",\r\n \"EdFiAdminConnectionString\": \"not-valid-connection-string\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Security connection string", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"title\");", + " pm.expect(response).to.have.property(\"errors\");", + " });", + "", + " pm.test(\"POST Tenants Invalid EdFiSecurityConnectionString: Response title is helpful and accurate\", function () {", + " pm.expect(response.title.toLowerCase()).to.contain(\"validation\");", + " });", + "", + " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages by property\", function () {", + " pm.expect(response.errors[\"EdFiSecurityConnectionString\"].length).to.equal(1);", + " });", + "", + " pm.test(\"POST Tenants Invalid ConnectionString: Response errors include messages with wrong elements\", function () {", + " pm.expect(response.errors[\"EdFiSecurityConnectionString\"][0]).to.contain(\"is not valid\");", + " });", + "}", + "else {", + " pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + " });", + "", + " pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"TenantName\": \"Tenant-{{TenantGUID}}\",\r\n \"EdFiSecurityConnectionString\": \"not-valid-connection-string\",\r\n \"EdFiAdminConnectionString\": \"{{connectionString}}\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + "});", + "", + "const GetTenantsSchema = {", + " \"type\": \"array\",", + " \"items\": [", + " {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + " ]", + "}", + "", + "pm.test(\"GET Tenants: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsSchema);", + "});", + "" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/tenants", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants" + ] + } + }, + "response": [] + }, + { + "name": "Tenants Invalid", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Invalid\", function () {", + " pm.response.to.have.status(404);", + "});", + "" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenant", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenant" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name - Multitenant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + " });", + "", + " const response = pm.response.json();", + " const result = pm.response.json();", + "", + " pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " const tenantName = pm.collectionVariables.get(\"CreatedTenantName\");", + "", + " pm.expect(result.tenantName).to.equal(tenantName);", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + " });", + "", + " const GetTenantsByTenantSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + "", + " pm.test(\"GET Tenants by Tenant Id: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsByTenantSchema);", + " });", + "}", + "else {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(404);", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/{{CreatedTenantName}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "{{CreatedTenantName}}" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name - Singletenant", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"false\") {", + " pm.test(\"GET Tenants by Tenant Id: Status code is Found\", function () {", + " pm.response.to.have.status(200);", + " });", + "", + " const response = pm.response.json();", + " const result = pm.response.json();", + "", + " pm.test(\"GET Tenant Name: Response result matches tenant\", function () {", + " pm.expect(result.tenantName).to.equal(\"default\");", + " pm.expect(result.adminConnectionString.host).to.not.equal(null);", + " pm.expect(result.adminConnectionString.database).to.not.equal(null);", + " pm.expect(result.securityConnectionString.host).to.not.equal(null);", + " pm.expect(result.securityConnectionString.database).to.not.equal(null);", + " });", + "", + " const GetTenantsByTenantSchema = {", + " \"type\": \"object\",", + " \"properties\": {", + " \"tenantName\": {", + " \"type\": \"string\"", + " },", + " \"adminConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " },", + " },", + " \"securityConnectionString\": {", + " \"type\": \"object\",", + " \"properties\": {", + " \"host\": {", + " \"type\": \"string\"", + " },", + " \"database\": {", + " \"type\": \"string\"", + " }", + " }", + " }", + " },", + " \"required\": [", + " \"tenantName\"", + " ]", + " }", + "", + " pm.test(\"GET Tenants by Tenant Id: Validation Schema Response\", () => {", + " pm.response.to.have.jsonSchema(GetTenantsByTenantSchema);", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/default", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "default" + ] + } + }, + "response": [] + }, + { + "name": "Tenants by Tenant Name Not Found", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"GET Tenants: Status code is Found\", function () {", + " pm.response.to.have.status(404);", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/notexistingtenantname", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "notexistingtenantname" + ] + } + }, + "response": [] + }, + { + "name": "Tenants - Invalid Api Mode", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + "});", + "", + "const response = pm.response.json();", + "", + "pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + "});", + "", + "pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"wrong api version for this instance mode.\");", + "});" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v1/tenants/{{CreatedTenantName}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v1", + "tenants", + "{{CreatedTenantName}}" + ] + } + }, + "response": [] + }, + { + "name": "Tenants", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " pm.test(\"DELETE Tenants: Status code is OK\", function () {", + " pm.response.to.have.status(200);", + " });", + "}", + "else {", + " pm.test(\"Status code is Bad Request\", function () {", + " pm.response.to.have.status(400);", + " });", + "", + " const response = pm.response.json();", + "", + " pm.test(\"Response matches error format\", function () {", + " pm.expect(response).to.have.property(\"message\");", + " });", + "", + " pm.test(\"Response title is helpful and accurate\", function () {", + " pm.expect(response.message.toLowerCase()).to.contain(\"not multitenant environment.\");", + " });", + "}" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{API_URL}}/v2/tenants/{{CreatedTenantName}}", + "host": [ + "{{API_URL}}" + ], + "path": [ + "v2", + "tenants", + "{{CreatedTenantName}}" + ] + } + }, + "response": [] + } + ] + } + ] + } + ], + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{TOKEN}}", + "type": "string" + } + ] + }, + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "function generateClientSecret() {", + " const minLength = 32;", + " const maxLength = 128;", + " let result = '';", + " const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';", + " const specialCharacters = '!@#$%^&*()_+{}:\"<>?|[];\\',./`~';", + " const length = Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;", + "", + " result += randomChar('abcdefghijklmnopqrstuvwxyz');", + " result += randomChar('ABCDEFGHIJKLMNOPQRSTUVWXYZ');", + " result += randomChar('0123456789');", + " result += randomChar(specialCharacters);", + "", + " for (let i = result.length; i < length; i++) {", + " const charactersPlusSpecial = characters + specialCharacters;", + " result += charactersPlusSpecial.charAt(Math.floor(Math.random() * charactersPlusSpecial.length));", + " }", + "", + " return shuffleString(result);", + "}", + "", + "function randomChar(str) {", + " return str.charAt(Math.floor(Math.random() * str.length));", + "}", + "", + "function shuffleString(str) {", + " const array = str.split('');", + " for (let i = array.length - 1; i > 0; i--) {", + " const j = Math.floor(Math.random() * (i + 1));", + " [array[i], array[j]] = [array[j], array[i]];", + " }", + " return array.join('');", + "}", + "", + "let guid = pm.variables.replaceIn('{{$guid}}');", + "let client_secret = generateClientSecret();", + "", + "let header = {", + " 'Content-Type': 'application/x-www-form-urlencoded'", + "};", + "", + "if (pm.variables.get(\"isMultitenant\") == \"true\") {", + " header['Tenant'] = `${pm.variables.get(\"tenant1\")}`;", + " pm.request.headers.upsert({key: 'Tenant', value: `${pm.variables.get(\"tenant1\")}` });", + "}", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/register`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'ClientId', value: guid },", + " {key: 'ClientSecret', value: client_secret },", + " {key: 'DisplayName', value: guid }", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + "", + "pm.sendRequest({", + " url: `${pm.variables.get(\"API_URL\")}/connect/token`,", + " method: 'POST',", + " header: header,", + " body: {", + " mode: 'urlencoded',", + " urlencoded: [", + " {key: 'client_id', value: guid },", + " {key: 'client_secret', value: client_secret },", + " {key: 'grant_type', value: \"client_credentials\"},", + " {key: 'scope', value: \"edfi_admin_api/full_access\"}", + " ]", + " }", + "},", + " (err, res) => {", + " error = res.json().error", + " if(error) {", + " throw res.json().error_description", + " }", + " pm.collectionVariables.set(\"TOKEN\", res.json().access_token)", + "});", + "});" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "packages": {}, + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "TOKEN", + "value": "" + } + ] +} \ No newline at end of file diff --git a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json index 5ccdf4851..151f9246c 100644 --- a/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json +++ b/Application/EdFi.Ods.AdminApi/E2E Tests/V2/Admin API.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "securityconnectionString", + "value": "host=localhost;port=5401;username=postgres;password=P@ssw0rd;database=EdFi_Security;pooling=false", + "type": "default", + "enabled": true + }, { "key": "isMultitenant", "value": "false", diff --git a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj index ae02f3e76..77491a8f0 100644 --- a/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj +++ b/Application/EdFi.Ods.AdminApi/EdFi.Ods.AdminApi.csproj @@ -51,7 +51,6 @@ - diff --git a/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs b/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs index a37f568a1..4148a618c 100644 --- a/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs +++ b/Application/EdFi.Ods.AdminApi/Features/FeatureConstants.cs @@ -28,6 +28,7 @@ public static class FeatureConstants public const string EdOrgIdsValidationMessage = "Please provide at least one education organization id."; public const string VendorIdValidationMessage = "Please provide valid vendor id."; public const string ClaimSetAlreadyExistsMessage = "A claim set with this name already exists in the database. Please enter a unique name."; + public const string TenantAlreadyExistsMessage = "A tenant with this name already exists. Please enter a unique name."; public const string ClaimSetNameMaxLengthMessage = "The claim set name must be less than 255 characters."; public const string ClaimSetNotFound = "No such claim set exists in the database."; public const string InvalidResourceClaimActions = "Please provide a valid resourceClaimActions object."; @@ -48,6 +49,7 @@ public static class FeatureConstants public const string OdsInstanceDerivativeCombinedKeyMustBeUnique = "The combined key ODS instance id and derivative type must be unique."; public const string OdsInstanceContextCombinedKeyMustBeUnique = "The combined key ODS instance id and context key must be unique."; public const string OdsInstanceConnectionStringInvalid = "The connection string is not valid."; + public const string TenantConnectionStringInvalid = "The connection string is not valid."; public const string OdsInstanceDerivativeIdDescription = "ODS instance derivative id."; public const string OdsInstanceDerivativeOdsInstanceIdDescription = "ODS instance derivative ODS instance id."; public const string OdsInstanceDerivativeDerivativeTypeDescription = "derivative type."; diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs new file mode 100644 index 000000000..bdab671a9 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/AddTenant.cs @@ -0,0 +1,116 @@ +// 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.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using FluentValidation; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +public class AddTenant : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapPost(endpoints, "/tenants", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(201)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle( + Validator validator, + AddTenantCommand addTenantCommand, + IMapper mapper, + AddTenantRequest request, + IServiceScopeFactory serviceScopeFactory, + IOptions options) + { + if (!options.Value.MultiTenancy) + return Results.BadRequest(new { message = "Not multitenant environment." }); + + await validator.GuardAsync(request); + + var model = mapper.Map(request); + addTenantCommand.Execute(model); + + await InitializeTenantsAsync(serviceScopeFactory); + + return Results.Created($"/tenants/{request.TenantName}", null); + } + + [SwaggerSchema(Title = "AddTenantRequest")] + public class AddTenantRequest : IAddTenantModel + { + [SwaggerSchema(Description = "The unique name of the tenant.", Nullable = false)] + public string TenantName { get; set; } = string.Empty; + + [SwaggerSchema(Description = "The connection string for EdFi_Security.", Nullable = true)] + public string? EdFiSecurityConnectionString { get; set; } + + [SwaggerSchema(Description = "The connection string for EdFi_Admin.", Nullable = true)] + public string? EdFiAdminConnectionString { get; set; } + } + + public class Validator : AbstractValidator + { + private readonly ITenantsService _tenantsService; + private readonly string _databaseEngine; + + public Validator([FromServices] ITenantsService tenantsService, IOptions options) + { + _tenantsService = tenantsService; + _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + RuleFor(x => x.TenantName) + .NotEmpty() + .MaximumLength(100) + .Must(BeAUniqueName) + .WithMessage(FeatureConstants.TenantAlreadyExistsMessage); + + RuleFor(m => m.EdFiAdminConnectionString) + .MaximumLength(500) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.TenantConnectionStringInvalid) + .When(m => !string.IsNullOrEmpty(m.EdFiAdminConnectionString)); + + RuleFor(m => m.EdFiSecurityConnectionString) + .MaximumLength(500) + .Must(BeAValidConnectionString) + .WithMessage(FeatureConstants.TenantConnectionStringInvalid) + .When(m => !string.IsNullOrEmpty(m.EdFiSecurityConnectionString)); + } + + private bool BeAUniqueName(string name) + { + var tenants = _tenantsService.GetTenantsAsync(true).Result; + return tenants.TrueForAll(x => x.TenantName != name); + } + + private bool BeAValidConnectionString(string? connectionString) + { + return ConnectionStringHelper.ValidateConnectionString(_databaseEngine, connectionString); + } + } + + private static async Task InitializeTenantsAsync(IServiceScopeFactory serviceScopeFactory) + { + using IServiceScope scope = serviceScopeFactory.CreateScope(); + + ITenantsService scopedProcessingService = + scope.ServiceProvider.GetRequiredService(); + + await scopedProcessingService.InitializeTenantsAsync(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs new file mode 100644 index 000000000..586c2670a --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/DeleteTenant.cs @@ -0,0 +1,66 @@ +// 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.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.Extensions; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Database.Commands; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +public class DeleteTenant : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder + .MapDelete(endpoints, "/tenants/{tenantName}", Handle) + .WithDefaultSummaryAndDescription() + .WithRouteOptions(b => b.WithResponseCode(200, FeatureCommonConstants.DeletedSuccessResponseDescription)) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task Handle( + [FromServices] ITenantsService iTenantsService, + DeleteTenantCommand deleteTenantCommand, + IServiceScopeFactory serviceScopeFactory, + IOptions options, + string tenantName) + { + if (!options.Value.MultiTenancy) + return Results.BadRequest(new { message = "Not multitenant environment." }); + + var tenant = await iTenantsService.GetTenantByTenantIdAsync(tenantName); + if (tenant == null) + return Results.NotFound(); + + deleteTenantCommand.Execute(tenantName); + + await InitializeTenantsAsync(serviceScopeFactory); + + return Results.Ok("Tenant".ToJsonObjectResponseDeleted()); + } + + [SwaggerSchema(Title = "DeleteTenantRequest")] + public class DeleteTenantRequest + { + [SwaggerSchema(Description = "The unique name of the tenant to delete.", Nullable = false)] + public string TenantName { get; set; } = string.Empty; + } + + private static async Task InitializeTenantsAsync(IServiceScopeFactory serviceScopeFactory) + { + using IServiceScope scope = serviceScopeFactory.CreateScope(); + + ITenantsService scopedProcessingService = + scope.ServiceProvider.GetRequiredService(); + + await scopedProcessingService.InitializeTenantsAsync(); + } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs new file mode 100644 index 000000000..4258b03f5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/ReadTenants.cs @@ -0,0 +1,101 @@ +// 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.Common.Features; +using EdFi.Ods.AdminApi.Common.Infrastructure; +using EdFi.Ods.AdminApi.Common.Infrastructure.ErrorHandling; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +public class ReadTenants : IFeature +{ + public void MapEndpoints(IEndpointRouteBuilder endpoints) + { + AdminApiEndpointBuilder.MapGet(endpoints, "/tenants", GetTenantsAsync) + .BuildForVersions(AdminApiVersions.V2); + + AdminApiEndpointBuilder.MapGet(endpoints, "/tenants/{tenantName}", GetTenantsByTenantIdAsync) + .BuildForVersions(AdminApiVersions.V2); + } + + public static async Task GetTenantsAsync([FromServices] ITenantsService tenantsService, IMemoryCache memoryCache, IOptions options) + { + var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var tenants = await tenantsService.GetTenantsAsync(true); + + var response = tenants + .Select(t => + { + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, t.ConnectionStrings.EdFiAdminConnectionString); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, t.ConnectionStrings.EdFiSecurityConnectionString); + + return new TenantsResponse + { + TenantName = t.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }; + }) + .ToList(); + + return Results.Ok(response); + } + + public static async Task GetTenantsByTenantIdAsync([FromServices] ITenantsService tenantsService, + IMemoryCache memoryCache, string tenantName, IOptions options) + { + var _databaseEngine = options.Value.DatabaseEngine ?? throw new NotFoundException("AppSettings", "DatabaseEngine"); + + var tenant = await tenantsService.GetTenantByTenantIdAsync(tenantName); + if (tenant == null) + return Results.NotFound(); + + var adminHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, tenant.ConnectionStrings.EdFiAdminConnectionString); + var securityHostAndDatabase = ConnectionStringHelper.GetHostAndDatabase(_databaseEngine, tenant.ConnectionStrings.EdFiSecurityConnectionString); + + return Results.Ok(new TenantsResponse + { + TenantName = tenant.TenantName, + AdminConnectionString = new EdfiConnectionString() + { + host = adminHostAndDatabase.Host, + database = adminHostAndDatabase.Database + }, + SecurityConnectionString = new EdfiConnectionString() + { + host = securityHostAndDatabase.Host, + database = securityHostAndDatabase.Database + } + }); + } +} + +public class TenantsResponse +{ + public string? TenantName { get; set; } + public EdfiConnectionString? AdminConnectionString { get; set; } + public EdfiConnectionString? SecurityConnectionString { get; set; } +} + +public class EdfiConnectionString +{ + public string? host { get; set; } + public string? database { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs new file mode 100644 index 000000000..0e9ee8dd5 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Features/Tenants/TenantModel.cs @@ -0,0 +1,38 @@ +// 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.Common.Constants; +using Swashbuckle.AspNetCore.Annotations; + +namespace EdFi.Ods.AdminApi.Features.Tenants; + +[SwaggerSchema] +public class TenantModel +{ + [SwaggerSchema(Description = Constants.TenantNameDescription, Nullable = false)] + public required string TenantName { get; set; } + + [SwaggerSchema(Description = Constants.TenantConnectionStringDescription, Nullable = false)] + public TenantModelConnectionStrings ConnectionStrings { get; set; } = new(); +} + +[SwaggerSchema] +public class TenantModelConnectionStrings +{ + public string EdFiSecurityConnectionString { get; set; } + public string EdFiAdminConnectionString { get; set; } + + public TenantModelConnectionStrings() + { + EdFiAdminConnectionString = string.Empty; + EdFiSecurityConnectionString = string.Empty; + } + + public TenantModelConnectionStrings(string edFiAdminConnectionString, string edFiSecurityConnectionString) + { + EdFiAdminConnectionString = edFiAdminConnectionString; + EdFiSecurityConnectionString = edFiSecurityConnectionString; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs new file mode 100644 index 000000000..deb15ee54 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/AddTenantCommand.cs @@ -0,0 +1,63 @@ +// 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; +using System.Text.Json.Nodes; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public class AddTenantCommand(IAppSettingsFileProvider fileProvider) +{ + private static readonly object _fileLock = new(); + private readonly IAppSettingsFileProvider _fileProvider = fileProvider; + + public virtual void Execute(IAddTenantModel model) + { + lock (_fileLock) + { + var json = _fileProvider.ReadAllText(); + try + { + var root = JsonNode.Parse(json) ?? throw new InvalidOperationException("appsettings.json is empty or invalid."); + + var tenantsNode = root["Tenants"] as JsonObject ?? throw new InvalidOperationException("Tenants section missing in appsettings.json."); + + if (tenantsNode.ContainsKey(model.TenantName)) + { + throw new InvalidOperationException($"Tenant '{model.TenantName}' already exists."); + } + + var tenantObj = new JsonObject + { + ["ConnectionStrings"] = new JsonObject + { + ["EdFi_Security"] = model.EdFiSecurityConnectionString, + ["EdFi_Admin"] = model.EdFiAdminConnectionString + } + }; + + tenantsNode[model.TenantName] = tenantObj; + + var options = new JsonSerializerOptions { WriteIndented = true }; + var updatedJson = root.ToJsonString(options); + + _fileProvider.WriteAllText(updatedJson); + } + catch (JsonException ex) + { + throw new InvalidOperationException("appsettings.json contains invalid JSON.", ex); + } + } + } + +} + +public interface IAddTenantModel +{ + public string TenantName { get; } + public string? EdFiSecurityConnectionString { get; } + public string? EdFiAdminConnectionString { get; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs new file mode 100644 index 000000000..a20166d76 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Database/Commands/DeleteTenantCommand.cs @@ -0,0 +1,52 @@ +// 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; +using System.Text.Json.Nodes; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; + +namespace EdFi.Ods.AdminApi.Infrastructure.Database.Commands; + +public class DeleteTenantCommand(IAppSettingsFileProvider fileProvider) +{ + private static readonly object _fileLock = new(); + + private readonly IAppSettingsFileProvider _fileProvider = fileProvider; + + public virtual void Execute(string tenantName) + { + lock (_fileLock) + { + var json = _fileProvider.ReadAllText(); + try + { + var root = JsonNode.Parse(json) ?? throw new InvalidOperationException("appsettings.json is empty or invalid."); + + var tenantsNode = root["Tenants"] as JsonObject ?? throw new InvalidOperationException("Tenants section missing in appsettings.json."); + + if (!tenantsNode.ContainsKey(tenantName)) + { + throw new InvalidOperationException($"Tenant '{tenantName}' does not exist."); + } + + tenantsNode.Remove(tenantName); + + var options = new JsonSerializerOptions { WriteIndented = true }; + var updatedJson = root.ToJsonString(options); + + _fileProvider.WriteAllText(updatedJson); + } + catch (JsonException ex) + { + throw new InvalidOperationException("appsettings.json contains invalid JSON.", ex); + } + } + } +} + +public interface IDeleteTenantModel +{ + public string TenantName { get; set; } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs new file mode 100644 index 000000000..199e10cd0 --- /dev/null +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Helpers/FileSystemAppSettingsFileProvider.cs @@ -0,0 +1,25 @@ +// 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.Infrastructure.Helpers; + +public interface IAppSettingsFileProvider +{ + string ReadAllText(); + void WriteAllText(string content); +} + +public class FileSystemAppSettingsFileProvider(string filePath) : IAppSettingsFileProvider +{ + public string ReadAllText() + { + return File.ReadAllText(filePath); + } + + public void WriteAllText(string content) + { + File.WriteAllText(filePath, content); + } +} diff --git a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs similarity index 54% rename from Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs rename to Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs index 4b122bfa8..afe214091 100644 --- a/Application/EdFi.Ods.AdminApi.AdminConsole/Infrastructure/Services/Tenants/TenantService.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/Services/Tenants/TenantService.cs @@ -1,103 +1,109 @@ -// 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.Dynamic; -using EdFi.Ods.AdminApi.AdminConsole.Features.Tenants; -using EdFi.Ods.AdminApi.Common.Constants; -using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; -using EdFi.Ods.AdminApi.Common.Settings; -using log4net; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Options; - -namespace EdFi.Ods.AdminApi.AdminConsole.Infrastructure.Services.Tenants; - -public interface IAdminConsoleTenantsService -{ - Task InitializeTenantsAsync(); - Task> GetTenantsAsync(bool fromCache = false); - Task GetTenantByTenantIdAsync(int tenantId); -} - -public class TenantService(IOptionsSnapshot options, - IMemoryCache memoryCache) : IAdminConsoleTenantsService -{ - private const string ADMIN_DB_KEY = "EdFi_Admin"; - protected AppSettingsFile _appSettings = options.Value; - private readonly IMemoryCache _memoryCache = memoryCache; - private static readonly ILog _log = LogManager.GetLogger(typeof(TenantService)); - - public async Task InitializeTenantsAsync() - { - var tenants = await GetTenantsAsync(); - //store it in memorycache - await Task.FromResult(_memoryCache.Set(AdminConsoleConstants.TenantsCacheKey, tenants)); - } - - public async Task> GetTenantsAsync(bool fromCache = false) - { - List results; - - if (fromCache) - { - results = await GetTenantsFromCacheAsync(); - if (results.Count > 0) - { - return results; - } - } - - results = []; - //check multitenancy - if (_appSettings.AppSettings.MultiTenancy) - { - var ordinalId = 1; - foreach (var tenantConfig in _appSettings.Tenants) - { - var connectionString = tenantConfig.Value.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value; - if (!ConnectionStringHelper.ValidateConnectionString(_appSettings.AppSettings.DatabaseEngine!, connectionString)) - { - _log.WarnFormat("Tenant {Key} has an invalid connection string for database {ADMIN_DB_KEY}. Database engine is {engine}", - tenantConfig.Key, ADMIN_DB_KEY, _appSettings.AppSettings.DatabaseEngine); - } - dynamic document = new ExpandoObject(); - document.edfiApiDiscoveryUrl = tenantConfig.Value.EdFiApiDiscoveryUrl; - document.name = tenantConfig.Key; - results.Add(new TenantModel() - { - TenantId = ordinalId, - Document = document, - }); - ordinalId++; - } - } - else - { - dynamic document = new ExpandoObject(); - document.edfiApiDiscoveryUrl = _appSettings.EdFiApiDiscoveryUrl; - document.name = "default"; - results.Add(new TenantModel() - { - TenantId = 1, - Document = document, - }); - } - return results; - } - - public async Task GetTenantByTenantIdAsync(int tenantId) - { - var tenants = await GetTenantsAsync(); - var tenant = tenants.FirstOrDefault(p => p.TenantId == tenantId); - return tenant; - } - - private async Task> GetTenantsFromCacheAsync() - { - var tenants = await Task.FromResult(_memoryCache.Get>(AdminConsoleConstants.TenantsCacheKey)); - return tenants ?? []; - } -} - +// 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.Common.Constants; +using EdFi.Ods.AdminApi.Common.Infrastructure.Helpers; +using EdFi.Ods.AdminApi.Common.Settings; +using EdFi.Ods.AdminApi.Features.Tenants; +using log4net; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; + +public interface ITenantsService +{ + Task InitializeTenantsAsync(); + Task> GetTenantsAsync(bool fromCache = false); + Task GetTenantByTenantIdAsync(string tenantName); +} + +public class TenantService(IOptionsSnapshot options, + IMemoryCache memoryCache) : ITenantsService +{ + private const string ADMIN_DB_KEY = "EdFi_Admin"; + private const string SECURITY_DB_KEY = "EdFi_Security"; + protected AppSettingsFile _appSettings = options.Value; + private readonly IMemoryCache _memoryCache = memoryCache; + private static readonly ILog _log = LogManager.GetLogger(typeof(TenantService)); + + public async Task InitializeTenantsAsync() + { + var tenants = await GetTenantsAsync(); + //store it in memorycache + await Task.FromResult(_memoryCache.Set(Constants.TenantsCacheKey, tenants)); + } + + public async Task> GetTenantsAsync(bool fromCache = false) + { + List results; + + if (fromCache) + { + results = await GetTenantsFromCacheAsync(); + if (results.Count > 0) + { + return results; + } + } + + results = []; + + if (_appSettings.AppSettings.MultiTenancy) + { + foreach (var tenantConfig in _appSettings.Tenants) + { + /// Admin database + var adminConnectionString = tenantConfig.Value.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value; + if (!ConnectionStringHelper.ValidateConnectionString(_appSettings.AppSettings.DatabaseEngine!, adminConnectionString)) + { + _log.WarnFormat("Tenant {Key} has an invalid connection string for database {ADMIN_DB_KEY}. Database engine is {engine}", + tenantConfig.Key, ADMIN_DB_KEY, _appSettings.AppSettings.DatabaseEngine); + } + + /// Security database + var securityConnectionString = tenantConfig.Value.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value; + if (!ConnectionStringHelper.ValidateConnectionString(_appSettings.AppSettings.DatabaseEngine!, securityConnectionString)) + { + _log.WarnFormat("Tenant {Key} has an invalid connection string for database {SECURITY_DB_KEY}. Database engine is {engine}", + tenantConfig.Key, SECURITY_DB_KEY, _appSettings.AppSettings.DatabaseEngine); + } + + results.Add(new TenantModel() + { + TenantName = tenantConfig.Key, + ConnectionStrings = new(adminConnectionString, securityConnectionString) + }); + } + } + else + { + results.Add(new TenantModel() + { + TenantName = Constants.DefaultTenantName, + ConnectionStrings = new TenantModelConnectionStrings + ( + edFiAdminConnectionString: _appSettings.ConnectionStrings.First(p => p.Key == ADMIN_DB_KEY).Value, + edFiSecurityConnectionString: _appSettings.ConnectionStrings.First(p => p.Key == SECURITY_DB_KEY).Value + ) + }); + } + + return results; + } + + public async Task GetTenantByTenantIdAsync(string tenantName) + { + var tenants = await GetTenantsAsync(); + var tenant = tenants.FirstOrDefault(p => p.TenantName.Equals(tenantName, StringComparison.OrdinalIgnoreCase)); + return tenant; + } + + private async Task> GetTenantsFromCacheAsync() + { + var tenants = await Task.FromResult(_memoryCache.Get>(Constants.TenantsCacheKey)); + return tenants ?? []; + } +} diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs index 6d5bcedf1..3c3af9ac0 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationBuilderExtensions.cs @@ -20,14 +20,15 @@ using EdFi.Ods.AdminApi.Common.Settings; using EdFi.Ods.AdminApi.Features.Connect; using EdFi.Ods.AdminApi.Infrastructure.Documentation; +using EdFi.Ods.AdminApi.Infrastructure.Helpers; using EdFi.Ods.AdminApi.Infrastructure.Security; +using EdFi.Ods.AdminApi.Infrastructure.Services.Tenants; using EdFi.Security.DataAccess.Contexts; using FluentValidation; using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Http.Json; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; using Microsoft.Net.Http.Headers; using Microsoft.OpenApi.Models; @@ -40,6 +41,11 @@ public static class WebApplicationBuilderExtensions public static void AddServices(this WebApplicationBuilder webApplicationBuilder) { webApplicationBuilder.Services.AddSingleton(); + + var env = webApplicationBuilder.Environment; + var appSettingsPath = Path.Combine(env.ContentRootPath, "appsettings.json"); + webApplicationBuilder.Services.AddSingleton(new FileSystemAppSettingsFileProvider(appSettingsPath)); + ConfigureRateLimiting(webApplicationBuilder); ConfigurationManager config = webApplicationBuilder.Configuration; webApplicationBuilder.Services.Configure(config.GetSection("AppSettings")); @@ -201,6 +207,10 @@ public static void AddServices(this WebApplicationBuilder webApplicationBuilder) webApplicationBuilder.Services.AddTransient(); webApplicationBuilder.Services.AddTransient(); + + webApplicationBuilder.Services.Configure(webApplicationBuilder.Configuration); + + webApplicationBuilder.Services.AddTransient(); } private static void EnableMultiTenancySupport(this WebApplicationBuilder webApplicationBuilder) diff --git a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs index e0b1120ec..bef144208 100644 --- a/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs +++ b/Application/EdFi.Ods.AdminApi/Infrastructure/WebApplicationExtensions.cs @@ -2,11 +2,9 @@ // 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.Helper; using EdFi.Ods.AdminApi.Common.Constants; -using static OpenIddict.Abstractions.OpenIddictConstants.Permissions; -using AdminApiV2Features = EdFi.Ods.AdminApi.Infrastructure.Helpers; using AdminApiV1Features = EdFi.Ods.AdminApi.V1.Infrastructure.Helpers; +using AdminApiV2Features = EdFi.Ods.AdminApi.Infrastructure.Helpers; namespace EdFi.Ods.AdminApi.Infrastructure; @@ -38,17 +36,6 @@ public static void MapFeatureEndpoints(this WebApplication application) } } - public static void MapAdminConsoleFeatureEndpoints(this WebApplication application) - { - application.UseEndpoints(endpoints => - { - foreach (var routeBuilder in AdminConsoleFeatureHelper.GetFeatures()) - { - routeBuilder.MapEndpoints(endpoints); - } - }); - } - public static void DefineSwaggerUIWithApiVersions(this WebApplication application, params string[] versions) { application.UseSwaggerUI(definitions => diff --git a/Application/EdFi.Ods.AdminApi/Program.cs b/Application/EdFi.Ods.AdminApi/Program.cs index 360b9675a..365d6f56f 100644 --- a/Application/EdFi.Ods.AdminApi/Program.cs +++ b/Application/EdFi.Ods.AdminApi/Program.cs @@ -3,8 +3,6 @@ // 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; -using EdFi.Ods.AdminApi.AdminConsole.Configurations; using EdFi.Ods.AdminApi.Common.Constants; using EdFi.Ods.AdminApi.Common.Infrastructure; using EdFi.Ods.AdminApi.Common.Infrastructure.MultiTenancy; @@ -30,28 +28,16 @@ // logging var _logger = LogManager.GetLogger("Program"); _logger.Info("Starting Admin API"); -var adminConsoleIsEnabled = builder.Configuration.GetValue("AppSettings:EnableAdminConsoleAPI"); var adminApiMode = builder.Configuration.GetValue("AppSettings:AdminApiMode", AdminApiMode.V2); var databaseEngine = builder.Configuration.GetValue("AppSettings:DatabaseEngine"); // Log configuration values as requested _logger.InfoFormat("Configuration - ApiMode: {0}, Engine: {1}", adminApiMode, databaseEngine); -//Order is important to enable CORS -if (adminConsoleIsEnabled && adminApiMode == AdminApiMode.V2) - builder.RegisterAdminConsoleCorsDependencies(_logger); - builder.AddServices(); -if (adminConsoleIsEnabled && adminApiMode == AdminApiMode.V2) - builder.RegisterAdminConsoleDependencies(); - var app = builder.Build(); -//Order is important to enable CORS -if (adminConsoleIsEnabled && adminApiMode == AdminApiMode.V2) - app.UseCorsForAdminConsole(); - var pathBase = app.Configuration.GetValue("AppSettings:PathBase"); if (!string.IsNullOrEmpty(pathBase)) { @@ -74,12 +60,6 @@ app.UseAuthorization(); app.MapFeatureEndpoints(); -//Map AdminConsole endpoints if the flag is enable -if (adminConsoleIsEnabled && adminApiMode == AdminApiMode.V2) -{ - app.MapAdminConsoleFeatureEndpoints(); -} - app.MapControllers(); app.UseHealthChecks("/health"); diff --git a/Application/EdFi.Ods.AdminApi/appsettings.Development.json b/Application/EdFi.Ods.AdminApi/appsettings.Development.json index f42389bdb..589118bda 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.Development.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.Development.json @@ -1,8 +1,8 @@ { "AppSettings": { - "MultiTenancy": false, - "EnableAdminConsoleAPI": true, - "DatabaseEngine": "SqlServer" + "MultiTenancy": true, + "DatabaseEngine": "SqlServer", + "IgnoresCertificateErrors": true }, "AdminConsoleSettings": { "CorsSettings": { @@ -21,8 +21,8 @@ "AllowRegistration": true }, "ConnectionStrings": { - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin;Integrated Security=True;Encrypt=false", - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security;Integrated Security=True;Encrypt=false" + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" }, "SwaggerSettings": { "EnableSwagger": true, @@ -37,17 +37,15 @@ "Tenants": { "tenant1": { "ConnectionStrings": { - "EdFi_Security": "host=localhost;port=5401;username=postgres;password=P@ssw0rd;database=EdFi_Security;application name=AdminApi;", - "EdFi_Admin": "host=localhost;port=5401;username=postgres;password=P@ssw0rd;database=EdFi_Admin;application name=AdminApi;" - }, - "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api6/" + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" + } }, "tenant2": { "ConnectionStrings": { - "EdFi_Security": "host=localhost;port=5402;username=postgres;password=P@ssw0rd;database=EdFi_Security;application name=AdminApi;", - "EdFi_Admin": "host=localhost;port=5402;username=postgres;password=P@ssw0rd;database=EdFi_Admin;application name=AdminApi;" - }, - "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api4/" + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin7;Integrated Security=True;Trusted_Connection=true;Encrypt=True;TrustServerCertificate=True" + } } }, "Testing": { diff --git a/Application/EdFi.Ods.AdminApi/appsettings.json b/Application/EdFi.Ods.AdminApi/appsettings.json index 7f442ad9e..7e3a8c281 100644 --- a/Application/EdFi.Ods.AdminApi/appsettings.json +++ b/Application/EdFi.Ods.AdminApi/appsettings.json @@ -1,96 +1,90 @@ { - "AppSettings": { - "DatabaseEngine": "SqlServer", - "EncryptionKey": "{ BASE_64_ENCRYPTION_KEY }", - "PathBase": "", - "DefaultPageSizeOffset": 0, - "DefaultPageSizeLimit": 25, - "MultiTenancy": false, - "PreventDuplicateApplications": false, - "EnableAdminConsoleAPI": true, - "EnableApplicationResetEndpoint": false, - "adminApiMode": "v2" // or "v1" + "AppSettings": { + "DatabaseEngine": "SqlServer", + "EncryptionKey": "{ BASE_64_ENCRYPTION_KEY }", + "PathBase": "", + "DefaultPageSizeOffset": 0, + "DefaultPageSizeLimit": 25, + "MultiTenancy": false, + "PreventDuplicateApplications": false, + "EnableApplicationResetEndpoint": false, + "adminApiMode": "v2" + }, + "AdminConsoleSettings": { + "ApplicationName": "Ed-Fi Health Check", + "ClaimsetName": "Ed-Fi Admin App", + "VendorCompany": "Ed-Fi Administrative Tools", + "VendorContactName": "", + "VendorNamespacePrefixes": "uri://ed-fi.org", + "CorsSettings": { + "EnableCors": false, + "AllowedOrigins": [ + "https://localhost" + ] }, - "AdminConsoleSettings": { - "ApplicationName": "Ed-Fi Health Check", - "ClaimsetName": "Ed-Fi Admin App", - "VendorCompany": "Ed-Fi Administrative Tools", - "VendorContactName": "", - "VendorNamespacePrefixes": "uri://ed-fi.org", - "CorsSettings": { - "EnableCors": false, - "AllowedOrigins": [ - "https://localhost" - ] - }, - "EncryptionKey": "abcdefghi" - }, - "Authentication": { - - "IssuerUrl": "", - "SigningKey": "", - "ValidateIssuerSigningKey": true, - "RoleClaimAttribute": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", - "AllowRegistration": true, - // V1: - "Authority": null - }, - "SwaggerSettings": { - "EnableSwagger": false, - "DefaultTenant": "" - }, - "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" - }, - "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api/", - "Log4NetCore": { - "Log4NetConfigFileName": "log4net/log4net.config" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "OpenIddict.*": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "Tenants": { - "tenant1": { - "ConnectionStrings": { - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True", - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True" - }, - "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api6/" - }, - "tenant2": { - "ConnectionStrings": { - "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True", - "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True" - }, - "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api4/" - } - }, - "AllowedHosts": "*", - - "IpRateLimiting": { - "EnableEndpointRateLimiting": true, - "StackBlockedRequests": false, - "RealIpHeader": "X-Real-IP", - "ClientIdHeader": "X-ClientId", - "HttpStatusCode": 429, - "IpWhitelist": [], - "EndpointWhitelist": [], - "GeneralRules": [ - { - "Endpoint": "POST:/Connect/Register", - "Period": "1m", - "Limit": 3 - } - ] + "EncryptionKey": "abcdefghi" + }, + "Authentication": { + "IssuerUrl": "", + "SigningKey": "", + "ValidateIssuerSigningKey": true, + "RoleClaimAttribute": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", + "AllowRegistration": true, + "Authority": null + }, + "SwaggerSettings": { + "EnableSwagger": false, + "DefaultTenant": "" + }, + "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" + }, + "EdFiApiDiscoveryUrl": "https://api.ed-fi.org/v7.2/api/", + "Log4NetCore": { + "Log4NetConfigFileName": "log4net/log4net.config" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "OpenIddict.*": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Tenants": { + "tenant1": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant1;Integrated Security=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant1;Integrated Security=True" + } }, - "Testing": { - "InjectException": false + "tenant2": { + "ConnectionStrings": { + "EdFi_Security": "Data Source=.\\;Initial Catalog=EdFi_Security_Tenant2;Integrated Security=True", + "EdFi_Admin": "Data Source=.\\;Initial Catalog=EdFi_Admin_Tenant2;Integrated Security=True" + } } + }, + "AllowedHosts": "*", + "IpRateLimiting": { + "EnableEndpointRateLimiting": true, + "StackBlockedRequests": false, + "RealIpHeader": "X-Real-IP", + "ClientIdHeader": "X-ClientId", + "HttpStatusCode": 429, + "IpWhitelist": [], + "EndpointWhitelist": [], + "GeneralRules": [ + { + "Endpoint": "POST:/Connect/Register", + "Period": "1m", + "Limit": 3 + } + ] + }, + "Testing": { + "InjectException": false + } } diff --git a/Docker/Settings/dev/mssql/run.sh b/Docker/Settings/dev/mssql/run.sh index 4201643be..6724b72d4 100644 --- a/Docker/Settings/dev/mssql/run.sh +++ b/Docker/Settings/dev/mssql/run.sh @@ -30,4 +30,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/dev/pgsql/run.sh b/Docker/Settings/dev/pgsql/run.sh index e2a35993d..4d06e4231 100644 --- a/Docker/Settings/dev/pgsql/run.sh +++ b/Docker/Settings/dev/pgsql/run.sh @@ -56,4 +56,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/mssql/run.sh b/Docker/Settings/mssql/run.sh index 77cc10825..aab016500 100644 --- a/Docker/Settings/mssql/run.sh +++ b/Docker/Settings/mssql/run.sh @@ -31,4 +31,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/Settings/pgsql/run.sh b/Docker/Settings/pgsql/run.sh index 1d845507e..00c92195e 100755 --- a/Docker/Settings/pgsql/run.sh +++ b/Docker/Settings/pgsql/run.sh @@ -34,4 +34,7 @@ if [[ -f /ssl/server.crt ]]; then update-ca-certificates fi +# Writing permissions for multitenant environment so the user can create tenants +chmod 664 /app/appsettings.json + dotnet EdFi.Ods.AdminApi.dll diff --git a/Docker/V1/Compose/mssql/compose-build-binaries.yml b/Docker/V1/Compose/mssql/compose-build-binaries.yml index 8a4857d90..7e1514417 100644 --- a/Docker/V1/Compose/mssql/compose-build-binaries.yml +++ b/Docker/V1/Compose/mssql/compose-build-binaries.yml @@ -51,7 +51,6 @@ services: AppSettings__DatabaseEngine: SqlServer AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: false AppSettings__EnableApplicationResetEndpoint: false AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: false @@ -63,7 +62,6 @@ services: AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V1/Compose/mssql/compose-build-dev.yml b/Docker/V1/Compose/mssql/compose-build-dev.yml index 8945e5c6e..c243bf004 100644 --- a/Docker/V1/Compose/mssql/compose-build-dev.yml +++ b/Docker/V1/Compose/mssql/compose-build-dev.yml @@ -107,7 +107,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: false AppSettings__EnableApplicationResetEndpoint: false AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: false @@ -118,7 +117,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/V1/Compose/pgsql/compose-build-binaries.yml b/Docker/V1/Compose/pgsql/compose-build-binaries.yml index f9fef083d..85ae8de46 100644 --- a/Docker/V1/Compose/pgsql/compose-build-binaries.yml +++ b/Docker/V1/Compose/pgsql/compose-build-binaries.yml @@ -47,7 +47,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: false AppSettings__EnableApplicationResetEndpoint: false AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: false @@ -59,7 +58,6 @@ services: AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" ConnectionStrings__EdFi_Admin: "host=admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V1/Compose/pgsql/compose-build-dev.yml b/Docker/V1/Compose/pgsql/compose-build-dev.yml index 2bba77d24..eda3bc541 100644 --- a/Docker/V1/Compose/pgsql/compose-build-dev.yml +++ b/Docker/V1/Compose/pgsql/compose-build-dev.yml @@ -82,7 +82,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: false AppSettings__EnableApplicationResetEndpoint: false AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: false @@ -92,7 +91,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "host=db-admin;port=5432;username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=db-admin;port=5432;username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml index 28234b908..6494dc0ec 100644 --- a/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-binaries-multi-tenant.yml @@ -35,7 +35,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -64,10 +63,8 @@ services: SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant1} Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml index 4933c3ea6..b6a2a9cdf 100644 --- a/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-dev-multi-tenant.yml @@ -41,7 +41,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: ${MULTITENANCY_ENABLED:-true} @@ -72,10 +71,8 @@ services: SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant2} Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml index 24c4fef8b..b3d76f28a 100644 --- a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml @@ -39,7 +39,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -66,10 +65,8 @@ services: SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant1} Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml index edf389d40..c8fec34f2 100644 --- a/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml +++ b/Docker/V2/Compose/mssql/MultiTenant/compose-build-idp-dev-multi-tenant.yml @@ -43,7 +43,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -72,10 +71,8 @@ services: SwaggerSettings__DefaultTenant: ${DEFAULT_TENANT:-tenant2} Tenants__tenant1__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant1,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "Data Source=db-admin-tenant2,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml index fdfd35a68..86fac53f8 100644 --- a/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-binaries.yml @@ -38,7 +38,6 @@ services: AppSettings__DatabaseEngine: SqlServer AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -50,7 +49,6 @@ services: AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml index 28b8f6dae..4af7944c0 100644 --- a/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-dev.yml @@ -43,7 +43,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: ${MULTITENANCY_ENABLED:-false} @@ -54,7 +53,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml index 763ab0a90..10c92c755 100644 --- a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-binaries.yml @@ -40,7 +40,6 @@ services: AppSettings__DatabaseEngine: SqlServer AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" AppSettings__PathBase: ${ADMIN_API_VIRTUAL_NAME:-adminapi} @@ -52,7 +51,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=$SQLSERVER_USER;Password=$SQLSERVER_PASSWORD; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml index 260588c4b..fcf974c28 100644 --- a/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml +++ b/Docker/V2/Compose/mssql/SingleTenant/compose-build-idp-dev.yml @@ -45,7 +45,6 @@ services: AppSettings__DatabaseEngine: "SqlServer" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -56,7 +55,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "Data Source=db-admin,1433;Initial Catalog=EdFi_Admin;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" ConnectionStrings__EdFi_Security: "Data Source=db-admin,1433;Initial Catalog=EdFi_Security;User Id=${SQLSERVER_USER};Password=${SQLSERVER_PASSWORD}; Integrated Security=False;Encrypt=false;TrustServerCertificate=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml index f0ac3c4a9..4959d1188 100644 --- a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-binaries-multi-tenant.yml @@ -37,7 +37,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -59,10 +58,8 @@ services: POSTGRES_USER: "${POSTGRES_USER}" Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" depends_on: - db-admin-tenant1 - db-admin-tenant2 diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml index 676cdc590..b92449799 100644 --- a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-dev-multi-tenant.yml @@ -40,7 +40,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -66,10 +65,8 @@ services: POSTGRES_USER: "${POSTGRES_USER}" Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml index c6ace890c..24d54b6c7 100644 --- a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-binaries-multi-tenant.yml @@ -38,7 +38,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -60,10 +59,8 @@ services: POSTGRES_USER: "${POSTGRES_USER}" Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" depends_on: - db-admin-tenant1 - db-admin-tenant2 diff --git a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml index ff7226d51..3e8896a92 100644 --- a/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml +++ b/Docker/V2/Compose/pgsql/MultiTenant/compose-build-idp-dev-multi-tenant.yml @@ -42,7 +42,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-true}" @@ -68,10 +67,8 @@ services: POSTGRES_USER: "${POSTGRES_USER}" Tenants__tenant1__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant1__ConnectionStrings__EdFi_Security: "host=db-admin-tenant1;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant1__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" Tenants__tenant2__ConnectionStrings__EdFi_Admin: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" Tenants__tenant2__ConnectionStrings__EdFi_Security: "host=db-admin-tenant2;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - Tenants__tenant2__EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" entrypoint: ["/bin/sh"] command: ["-c","/app/run.sh"] depends_on: diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml index 84ee6f58f..b61225bfd 100644 --- a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-binaries.yml @@ -36,7 +36,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -48,7 +47,6 @@ services: AdminConsoleSettings__CorsSettings__EnableCors: "${ENABLE_CORS:-false}" ConnectionStrings__EdFi_Admin: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml index 2b12c21d2..5dd09d27d 100644 --- a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-dev.yml @@ -40,7 +40,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -50,7 +49,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT:-5432};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml index 64f5943c4..d709b23f4 100644 --- a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-binaries.yml @@ -40,7 +40,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -50,7 +49,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=pb-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} IpRateLimiting__RealIpHeader: ${IPRATELIMITING__REALIPHEADER:-X-Real-IP} diff --git a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml index 982ec350b..6da92de72 100644 --- a/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml +++ b/Docker/V2/Compose/pgsql/SingleTenant/compose-build-idp-dev.yml @@ -42,7 +42,6 @@ services: AppSettings__DatabaseEngine: "PostgreSql" AppSettings__DefaultPageSizeLimit: ${PAGING_LIMIT:-25} AppSettings__DefaultPageSizeOffset: ${PAGING_OFFSET:-0} - AppSettings__EnableAdminConsoleAPI: ${ENABLE_ADMIN_CONSOLE:-true} AppSettings__EnableApplicationResetEndpoint: ${ENABLE_APPLICATION_RESET_ENDPOINT:-true} AppSettings__EncryptionKey: "TDMyNH0lJmo7aDRnNXYoSmAwSXQpV09nbitHSWJTKn0=" AppSettings__MultiTenancy: "${MULTITENANCY_ENABLED:-false}" @@ -53,7 +52,6 @@ services: Authentication__SigningKey: ${SIGNING_KEY} ConnectionStrings__EdFi_Admin: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Admin;pooling=true" ConnectionStrings__EdFi_Security: "host=db-admin;port=${POSTGRES_PORT};username=${POSTGRES_USER};password=${POSTGRES_PASSWORD};database=EdFi_Security;pooling=true" - EdFiApiDiscoveryUrl: "${EDFI_API_DISCOVERY_URL:-https://host.docker.internal/api/}" EnableDockerEnvironment: true IpRateLimiting__EnableEndpointRateLimiting: ${IPRATELIMITING__ENABLEENDPOINTRATELIMITING:-false} IpRateLimiting__StackBlockedRequests: ${IPRATELIMITING__STACKBLOCKEDREQUESTS:-false} diff --git a/Docker/dev.mssql.Dockerfile b/Docker/dev.mssql.Dockerfile index 737e3c8ee..b55f4ff26 100644 --- a/Docker/dev.mssql.Dockerfile +++ b/Docker/dev.mssql.Dockerfile @@ -18,9 +18,6 @@ COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi/ COPY --from=assets ./Application/EdFi.Ods.AdminApi EdFi.Ods.AdminApi/ RUN rm -f EdFi.Ods.AdminApi/appsettings.Development.json -COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.AdminConsole/ -COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminApi.AdminConsole/ - COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ @@ -31,11 +28,6 @@ RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT RUN dotnet restore && dotnet build -c Release RUN dotnet publish -c Release /p:EnvironmentName=$ASPNETCORE_ENVIRONMENT --no-build -o /app/EdFi.Ods.AdminApi -WORKDIR /source/EdFi.Ods.AdminApi.AdminConsole -RUN export ASPNETCORE_ENVIRONMENT=$ASPNETCORE_ENVIRONMENT -RUN dotnet restore && dotnet build -c Release -RUN dotnet publish -c Release /p:EnvironmentName=$ASPNETCORE_ENVIRONMENT --no-build -o /app/EdFi.Ods.AdminApi.AdminConsole - FROM mcr.microsoft.com/dotnet/aspnet:8.0.8-alpine3.20-amd64@sha256:98fa594b91cda6cac28d2aae25567730db6f8857367fab7646bdda91bc784b5f AS runtimebase RUN apk upgrade --no-cache && \ apk add dos2unix=~7 bash=~5 gettext=~0 icu=~74 curl musl=~1.2.5-r1 && \ diff --git a/Docker/dev.pgsql.Dockerfile b/Docker/dev.pgsql.Dockerfile index 53d6b1938..6e83ffcca 100644 --- a/Docker/dev.pgsql.Dockerfile +++ b/Docker/dev.pgsql.Dockerfile @@ -18,9 +18,6 @@ COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi/ COPY --from=assets ./Application/EdFi.Ods.AdminApi EdFi.Ods.AdminApi/ RUN rm -f EdFi.Ods.AdminApi/appsettings.Development.json -COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.AdminConsole/ -COPY --from=assets ./Application/EdFi.Ods.AdminApi.AdminConsole EdFi.Ods.AdminApi.AdminConsole/ - COPY --from=assets ./Application/NuGet.Config EdFi.Ods.AdminApi.Common/ COPY --from=assets ./Application/EdFi.Ods.AdminApi.Common EdFi.Ods.AdminApi.Common/ diff --git a/docs/http/claimsets.http b/docs/http/claimsets.http new file mode 100644 index 000000000..d0ba9275d --- /dev/null +++ b/docs/http/claimsets.http @@ -0,0 +1,37 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +@adminapi_url=https://localhost:7214 +@adminapi_client=adminapi_client2 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + + +### Get claimsets V1 +GET {{adminapi_url}}/v1/claimsets +Content-Type: application/json +Authorization: bearer {{token}} + + +### Get claimsets V2 +GET {{adminapi_url}}/v2/claimsets +Content-Type: application/json +Authorization: bearer {{token}} diff --git a/docs/http/tenants.http b/docs/http/tenants.http new file mode 100644 index 000000000..333215cce --- /dev/null +++ b/docs/http/tenants.http @@ -0,0 +1,62 @@ +# This file is intended for use with Admin API 2 running in multi-tenant mode, +# along with ODS/API 7.x in multi-tenant mode. It assumes there are two +# different tenants named "tenant1" and "tenant2". Each has only one ODS +# instance. + +# @adminapi_url=https://localhost/adminapi +@adminapi_url=https://localhost/adminapi + +@adminapi_client=adminapi_client22 +@adminapi_secret=adminapi_SECRET_2025_rftyguhijkotgyhuijok + +### Register a new client +POST {{adminapi_url}}/connect/register +Content-Type: application/x-www-form-urlencoded +Tenant: tenant1 + +ClientId={{adminapi_client}}&ClientSecret={{adminapi_secret}}&DisplayName=Admin+API+{{adminapi_client}} + +### Get a token +# @name tokenRequest +POST {{adminapi_url}}/connect/token +Content-Type: application/x-www-form-urlencoded +Authorization: basic {{adminapi_client}}:{{adminapi_secret}} +Tenant: tenant1 + +grant_type=client_credentials&scope=edfi_admin_api/full_access + +### +@token={{tokenRequest.response.body.access_token}} + +### Get tenants V2 +GET {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} + +### Get tenant V2 by id +GET {{adminapi_url}}/v2/tenants/default +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +### Create tenant +POST {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +{ + "TenantName": "tenant3", + "EdFiSecurityConnectionString": "123", + "EdFiAdminConnectionString": "Data Source=db-admin;Initial Catalog=EdFi_Admin;User Id=edfi;Password=P@55w0rd;Encrypt=false;TrustServerCertificate=true" +} + +### Create tenant no connection strings +POST {{adminapi_url}}/v2/tenants +Content-Type: application/json +Authorization: bearer {{token}} +Tenant: tenant1 + +{ + "TenantName": "tenantNoConnStrings2" +} \ No newline at end of file