diff --git a/bitwarden-server.sln b/bitwarden-server.sln index dbc37372a1..8545538259 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -135,6 +135,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedWeb.Test", "test\SharedWeb.Test\SharedWeb.Test.csproj", "{AD59537D-5259-4B7A-948F-0CF58E80B359}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SeederApi", "util\SeederApi\SeederApi.csproj", "{9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -343,6 +345,10 @@ Global {AD59537D-5259-4B7A-948F-0CF58E80B359}.Debug|Any CPU.Build.0 = Debug|Any CPU {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.ActiveCfg = Release|Any CPU {AD59537D-5259-4B7A-948F-0CF58E80B359}.Release|Any CPU.Build.0 = Release|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -398,6 +404,7 @@ Global {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {AD59537D-5259-4B7A-948F-0CF58E80B359} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9F08DFBB-482B-4C9D-A5F4-6BDA6EC2E68F} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/dev/setup_secrets.ps1 b/dev/setup_secrets.ps1 old mode 100644 new mode 100755 index 96dff04632..5013ca8bac --- a/dev/setup_secrets.ps1 +++ b/dev/setup_secrets.ps1 @@ -2,7 +2,7 @@ # Helper script for applying the same user secrets to each project param ( [switch]$clear, - [Parameter(ValueFromRemainingArguments = $true, Position=1)] + [Parameter(ValueFromRemainingArguments = $true, Position = 1)] $cmdArgs ) @@ -16,17 +16,18 @@ if ($clear -eq $true) { } $projects = @{ - Admin = "../src/Admin" - Api = "../src/Api" - Billing = "../src/Billing" - Events = "../src/Events" - EventsProcessor = "../src/EventsProcessor" - Icons = "../src/Icons" - Identity = "../src/Identity" - Notifications = "../src/Notifications" - Sso = "../bitwarden_license/src/Sso" - Scim = "../bitwarden_license/src/Scim" + Admin = "../src/Admin" + Api = "../src/Api" + Billing = "../src/Billing" + Events = "../src/Events" + EventsProcessor = "../src/EventsProcessor" + Icons = "../src/Icons" + Identity = "../src/Identity" + Notifications = "../src/Notifications" + Sso = "../bitwarden_license/src/Sso" + Scim = "../bitwarden_license/src/Scim" IntegrationTests = "../test/Infrastructure.IntegrationTest" + SeederApi = "../util/SeederApi" } foreach ($key in $projects.keys) { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 58ce0466c3..1e657613b8 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -86,12 +86,12 @@ namespace Bit.SharedWeb.Utilities; public static class ServiceCollectionExtensions { - public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings) + public static SupportedDatabaseProviders AddDatabaseRepositories(this IServiceCollection services, GlobalSettings globalSettings, bool forceEf = false) { var (provider, connectionString) = GetDatabaseProvider(globalSettings); services.SetupEntityFramework(connectionString, provider); - if (provider != SupportedDatabaseProviders.SqlServer) + if (provider != SupportedDatabaseProviders.SqlServer && !forceEf) { services.AddPasswordManagerEFRepositories(globalSettings.SelfHosted); } diff --git a/util/SeederApi/Controllers/InfoController.cs b/util/SeederApi/Controllers/InfoController.cs new file mode 100644 index 0000000000..de4a264ddb --- /dev/null +++ b/util/SeederApi/Controllers/InfoController.cs @@ -0,0 +1,20 @@ +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.SeederApi.Controllers; + +public class InfoController : Controller +{ + [HttpGet("~/alive")] + [HttpGet("~/now")] + public DateTime GetAlive() + { + return DateTime.UtcNow; + } + + [HttpGet("~/version")] + public JsonResult GetVersion() + { + return Json(AssemblyHelpers.GetVersion()); + } +} diff --git a/util/SeederApi/Controllers/SeedController.cs b/util/SeederApi/Controllers/SeedController.cs new file mode 100644 index 0000000000..1022db8a7f --- /dev/null +++ b/util/SeederApi/Controllers/SeedController.cs @@ -0,0 +1,125 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.AspNetCore.Mvc; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Text.Json; + +namespace Bit.SeederApi.Controllers; + +public class SeedRequestModel +{ + [Required] + public required string Template { get; set; } + public JsonElement? Arguments { get; set; } +} + +[Route("")] +public class SeedController : Controller +{ + private readonly ILogger _logger; + private readonly DatabaseContext _databaseContext; + + public SeedController(ILogger logger, DatabaseContext databaseContext) + { + _logger = logger; + _databaseContext = databaseContext; + } + + [HttpPost("/seed")] + public IActionResult Seed([FromBody] SeedRequestModel request) + { + _logger.LogInformation("Seeding with template: {Template}", request.Template); + + try + { + // Find the recipe class + var recipeTypeName = $"Bit.Seeder.Recipes.{request.Template}"; + var recipeType = Assembly.Load("Seeder") + .GetTypes() + .FirstOrDefault(t => t.FullName == recipeTypeName); + + if (recipeType == null) + { + return NotFound(new { Error = $"Recipe '{request.Template}' not found" }); + } + + // Instantiate the recipe with DatabaseContext + var recipeInstance = Activator.CreateInstance(recipeType, _databaseContext); + if (recipeInstance == null) + { + return StatusCode(500, new { Error = "Failed to instantiate recipe" }); + } + + // Find the Seed method + var seedMethod = recipeType.GetMethod("Seed"); + if (seedMethod == null) + { + return StatusCode(500, new { Error = $"Seed method not found in recipe '{request.Template}'" }); + } + + // Parse arguments and match to method parameters + var parameters = seedMethod.GetParameters(); + var arguments = new object?[parameters.Length]; + + if (request.Arguments == null && parameters.Length > 0) + { + return BadRequest(new { Error = "Arguments are required for this recipe" }); + } + + for (int i = 0; i < parameters.Length; i++) + { + var parameter = parameters[i]; + var parameterName = parameter.Name!; + + if (request.Arguments?.TryGetProperty(parameterName, out JsonElement value) == true) + { + try + { + arguments[i] = JsonSerializer.Deserialize(value.GetRawText(), parameter.ParameterType); + } + catch (JsonException ex) + { + return BadRequest(new + { + Error = $"Failed to deserialize parameter '{parameterName}'", + Details = ex.Message + }); + } + } + else if (!parameter.HasDefaultValue) + { + return BadRequest(new { Error = $"Missing required parameter: {parameterName}" }); + } + else + { + arguments[i] = parameter.DefaultValue; + } + } + + // Invoke the Seed method + var result = seedMethod.Invoke(recipeInstance, arguments); + + return Ok(new + { + Message = "Seed completed successfully", + Template = request.Template, + Result = result + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error seeding with template: {Template}", request.Template); + return StatusCode(500, new + { + Error = "An error occurred while seeding", + Details = ex.InnerException?.Message ?? ex.Message + }); + } + } + + [HttpGet("/delete")] + public string Delete() + { + return "hello delete"; + } +} diff --git a/util/SeederApi/Program.cs b/util/SeederApi/Program.cs new file mode 100644 index 0000000000..ccaedd31a8 --- /dev/null +++ b/util/SeederApi/Program.cs @@ -0,0 +1,37 @@ +using Bit.SharedWeb.Utilities; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers(); + +// Configure GlobalSettings from appsettings +var globalSettings = builder.Services.AddGlobalSettingsServices(builder.Configuration, builder.Environment); + +// Data Protection +builder.Services.AddCustomDataProtectionServices(builder.Environment, globalSettings); + +// Repositories +builder.Services.AddDatabaseRepositories(globalSettings); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (!app.Environment.IsDevelopment()) +{ + app.UseExceptionHandler("/Home/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); +} + +app.UseRouting(); + +app.MapControllerRoute( + name: "seed", + pattern: "{controller=Seed}/{action=Index}/{id?}"); + +app.MapControllerRoute( + name: "info", + pattern: "{controller=Info}/{action=Index}/{id?}"); + +app.Run(); diff --git a/util/SeederApi/Properties/launchSettings.json b/util/SeederApi/Properties/launchSettings.json new file mode 100644 index 0000000000..ecb2583d97 --- /dev/null +++ b/util/SeederApi/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50467", + "sslPort": 0 + } + }, + "profiles": { + "SeederApi": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5047", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/util/SeederApi/SeederApi.csproj b/util/SeederApi/SeederApi.csproj new file mode 100644 index 0000000000..2c0655fbac --- /dev/null +++ b/util/SeederApi/SeederApi.csproj @@ -0,0 +1,17 @@ + + + + bitwarden-seeder-api + net8.0 + enable + enable + false + + + + + + + + + diff --git a/util/SeederApi/appsettings.Development.json b/util/SeederApi/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/util/SeederApi/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/util/SeederApi/appsettings.json b/util/SeederApi/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/util/SeederApi/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}