1
0
mirror of https://github.com/bitwarden/server synced 2025-12-16 00:03:54 +00:00

Stricter scene and query types

SeederAPI only serves Scenes, Recipes are inteded to be used locally only.
This commit is contained in:
Matt Gibson
2025-10-29 12:27:15 -07:00
parent 16ee5cfaad
commit 878b78b51e
14 changed files with 261 additions and 204 deletions

View File

@@ -1,37 +1,36 @@
using Bit.SeederApi.Models.Requests;
using Bit.SeederApi.Models.Requests;
using Bit.SeederApi.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.SeederApi.Controllers
namespace Bit.SeederApi.Controllers;
[Route("query")]
public class QueryController(ILogger<QueryController> logger, ISeedService recipeService)
: Controller
{
[Route("query")]
public class QueryController(ILogger<QueryController> logger, IRecipeService recipeService)
: Controller
[HttpPost]
public IActionResult Query([FromBody] QueryRequestModel request)
{
[HttpPost]
public IActionResult Query([FromBody] QueryRequestModel request)
logger.LogInformation("Executing query: {Query}", request.Template);
try
{
logger.LogInformation("Executing query: {Query}", request.Template);
var result = recipeService.ExecuteQuery(request.Template, request.Arguments);
try
return Json(new { Result = result });
}
catch (RecipeNotFoundException ex)
{
return NotFound(new { Error = ex.Message });
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error executing query: {Query}", request.Template);
return BadRequest(new
{
var result = recipeService.ExecuteQuery(request.Template, request.Arguments);
return Json(new { Result = result });
}
catch (RecipeNotFoundException ex)
{
return NotFound(new { Error = ex.Message });
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error executing query: {Query}", request.Template);
return BadRequest(new
{
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
}
}

View File

@@ -1,139 +1,133 @@
using Bit.SeederApi.Models.Requests;
using Bit.SeederApi.Models.Response;
using Bit.SeederApi.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.SeederApi.Controllers
namespace Bit.SeederApi.Controllers;
[Route("seed")]
public class SeedController(ILogger<SeedController> logger, ISeedService recipeService)
: Controller
{
[Route("seed")]
public class SeedController(ILogger<SeedController> logger, IRecipeService recipeService)
: Controller
[HttpPost]
public IActionResult Seed([FromBody] SeedRequestModel request)
{
[HttpPost]
public IActionResult Seed([FromBody] SeedRequestModel request)
logger.LogInformation("Seeding with template: {Template}", request.Template);
try
{
logger.LogInformation("Seeding with template: {Template}", request.Template);
var response = recipeService.ExecuteScene(request.Template, request.Arguments);
try
{
var (result, seedId) = recipeService.ExecuteRecipe(request.Template, request.Arguments);
return Json(new SeedResponseModel
{
SeedId = seedId,
Result = result,
});
}
catch (RecipeNotFoundException ex)
{
return NotFound(new { Error = ex.Message });
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
return BadRequest(new
{
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
return Json(response);
}
[HttpDelete("batch")]
public async Task<IActionResult> DeleteBatch([FromBody] List<Guid> seedIds)
catch (RecipeNotFoundException ex)
{
logger.LogInformation("Deleting batch of seeded data with IDs: {SeedIds}", string.Join(", ", seedIds));
var aggregateException = new AggregateException();
await Task.Run(async () =>
{
foreach (var seedId in seedIds)
{
try
{
await recipeService.DestroyRecipe(seedId);
}
catch (Exception ex)
{
aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
}
}
});
if (aggregateException.InnerExceptions.Count > 0)
{
return BadRequest(new
{
Error = "One or more errors occurred while deleting seeded data",
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
});
}
return Ok(new
{
Message = "Batch delete completed successfully"
});
return NotFound(new { Error = ex.Message });
}
[HttpDelete("{seedId}")]
public async Task<IActionResult> Delete([FromRoute] Guid seedId)
catch (RecipeExecutionException ex)
{
logger.LogInformation("Deleting seeded data with ID: {SeedId}", seedId);
try
logger.LogError(ex, "Error executing scene: {Template}", request.Template);
return BadRequest(new
{
var result = await recipeService.DestroyRecipe(seedId);
return Json(result);
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
return BadRequest(new
{
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
}
[HttpDelete]
public async Task<IActionResult> DeleteAll()
{
logger.LogInformation("Deleting all seeded data");
// Pull all Seeded Data ids
var seededData = recipeService.GetAllSeededData();
var aggregateException = new AggregateException();
await Task.Run(async () =>
{
foreach (var sd in seededData)
{
try
{
await recipeService.DestroyRecipe(sd.Id);
}
catch (Exception ex)
{
aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", sd.Id);
}
}
Error = ex.Message,
Details = ex.InnerException?.Message
});
if (aggregateException.InnerExceptions.Count > 0)
{
return BadRequest(new
{
Error = "One or more errors occurred while deleting seeded data",
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
});
}
return NoContent();
}
}
[HttpDelete("batch")]
public async Task<IActionResult> DeleteBatch([FromBody] List<Guid> seedIds)
{
logger.LogInformation("Deleting batch of seeded data with IDs: {SeedIds}", string.Join(", ", seedIds));
var aggregateException = new AggregateException();
await Task.Run(async () =>
{
foreach (var seedId in seedIds)
{
try
{
await recipeService.DestroyRecipe(seedId);
}
catch (Exception ex)
{
aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
}
}
});
if (aggregateException.InnerExceptions.Count > 0)
{
return BadRequest(new
{
Error = "One or more errors occurred while deleting seeded data",
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
});
}
return Ok(new
{
Message = "Batch delete completed successfully"
});
}
[HttpDelete("{seedId}")]
public async Task<IActionResult> Delete([FromRoute] Guid seedId)
{
logger.LogInformation("Deleting seeded data with ID: {SeedId}", seedId);
try
{
var result = await recipeService.DestroyRecipe(seedId);
return Json(result);
}
catch (RecipeExecutionException ex)
{
logger.LogError(ex, "Error deleting seeded data: {SeedId}", seedId);
return BadRequest(new
{
Error = ex.Message,
Details = ex.InnerException?.Message
});
}
}
[HttpDelete]
public async Task<IActionResult> DeleteAll()
{
logger.LogInformation("Deleting all seeded data");
// Pull all Seeded Data ids
var seededData = recipeService.GetAllSeededData();
var aggregateException = new AggregateException();
await Task.Run(async () =>
{
foreach (var sd in seededData)
{
try
{
await recipeService.DestroyRecipe(sd.Id);
}
catch (Exception ex)
{
aggregateException = new AggregateException(aggregateException, ex);
logger.LogError(ex, "Error deleting seeded data: {SeedId}", sd.Id);
}
}
});
if (aggregateException.InnerExceptions.Count > 0)
{
return BadRequest(new
{
Error = "One or more errors occurred while deleting seeded data",
Details = aggregateException.InnerExceptions.Select(e => e.Message).ToList()
});
}
return NoContent();
}
}

View File

@@ -1,4 +1,4 @@
using System.Reflection;
using System.Reflection;
using Bit.Seeder;
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -12,11 +12,15 @@ public static class ServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddScenes(this IServiceCollection services)
{
var iSceneType1 = typeof(IScene<>);
var iSceneType2 = typeof(IScene<,>);
var isIScene = (Type t) => t == iSceneType1 || t == iSceneType2;
var seederAssembly = Assembly.Load("Seeder");
var sceneTypes = seederAssembly.GetTypes()
.Where(t => t is { IsClass: true, IsAbstract: false } &&
t.GetInterfaces().Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition().Name == "IScene`1"));
isIScene(i.GetGenericTypeDefinition())));
foreach (var sceneType in sceneTypes)
{
@@ -33,11 +37,12 @@ public static class ServiceCollectionExtensions
/// </summary>
public static IServiceCollection AddQueries(this IServiceCollection services)
{
var iQueryType = typeof(IQuery<,>);
var seederAssembly = Assembly.Load("Seeder");
var queryTypes = seederAssembly.GetTypes()
.Where(t => t is { IsClass: true, IsAbstract: false } &&
t.GetInterfaces().Any(i => i.IsGenericType &&
i.GetGenericTypeDefinition().Name == "IQuery`1"));
i.GetGenericTypeDefinition() == iQueryType));
foreach (var queryType in queryTypes)
{

View File

@@ -1,7 +1,20 @@
namespace Bit.SeederApi.Models.Response;
using Bit.Seeder;
public class SeedResponseModel
namespace Bit.SeederApi.Models.Response;
public class SceneResponseModel
{
public Guid? SeedId { get; set; }
public object? Result { get; set; }
public required Guid? SeedId { get; init; }
public required Dictionary<string, string?>? MangleMap { get; init; }
public required object? Result { get; init; }
public static SceneResponseModel FromSceneResult<T>(SceneResult<T> sceneResult, Guid? seedId)
{
return new SceneResponseModel
{
Result = sceneResult.Result,
MangleMap = sceneResult.MangleMap,
SeedId = seedId
};
}
}

View File

@@ -19,7 +19,7 @@ builder.Services.AddScoped<Microsoft.AspNetCore.Identity.IPasswordHasher<Bit.Cor
// Seeder services
builder.Services.AddSingleton<Bit.RustSDK.RustSdkService>();
builder.Services.AddScoped<Bit.Seeder.Factories.UserSeeder>();
builder.Services.AddScoped<IRecipeService, RecipeService>();
builder.Services.AddScoped<ISeedService, SeedService>();
builder.Services.AddScoped<MangleId>(_ => new MangleId());
builder.Services.AddScenes();
builder.Services.AddQueries();

View File

@@ -1,9 +1,10 @@
using System.Text.Json;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.SeederApi.Models.Response;
namespace Bit.SeederApi.Services;
public interface IRecipeService
public interface ISeedService
{
/// <summary>
/// Executes a scene with the given template name and arguments.
@@ -13,7 +14,7 @@ public interface IRecipeService
/// <returns>A tuple containing the result and optional seed ID for tracked entities</returns>
/// <exception cref="RecipeNotFoundException">Thrown when the scene template is not found</exception>
/// <exception cref="RecipeExecutionException">Thrown when there's an error executing the scene</exception>
(object? Result, Guid? SeedId) ExecuteRecipe(string templateName, JsonElement? arguments);
SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments);
/// <summary>
/// Destroys data created by a scene using the seeded data ID.

View File

@@ -3,16 +3,17 @@ using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Seeder;
using Bit.SeederApi.Models.Response;
namespace Bit.SeederApi.Services;
public class RecipeService(
public class SeedService(
DatabaseContext databaseContext,
ILogger<RecipeService> logger,
ILogger<SeedService> logger,
IServiceProvider serviceProvider,
IUserRepository userRepository,
IOrganizationRepository organizationRepository)
: IRecipeService
: ISeedService
{
private static readonly JsonSerializerOptions _jsonOptions = new()
{
@@ -25,13 +26,13 @@ public class RecipeService(
return databaseContext.SeededData.ToList();
}
public (object? Result, Guid? SeedId) ExecuteRecipe(string templateName, JsonElement? arguments)
public SceneResponseModel ExecuteScene(string templateName, JsonElement? arguments)
{
var result = ExecuteRecipeMethod(templateName, arguments, "Seed");
var result = ExecuteSceneMethod(templateName, arguments, "Seed");
if (result.TrackedEntities.Count == 0)
{
return (Result: result.Result, SeedId: null);
return SceneResponseModel.FromSceneResult(result, seedId: null);
}
var seededData = new SeededData
@@ -48,7 +49,7 @@ public class RecipeService(
logger.LogInformation("Saved seeded data with ID {SeedId} for scene {RecipeName}",
seededData.Id, templateName);
return (Result: result.Result, SeedId: seededData.Id);
return SceneResponseModel.FromSceneResult(result, seededData.Id);
}
public object ExecuteQuery(string queryName, JsonElement? arguments)
@@ -166,7 +167,7 @@ public class RecipeService(
return new { SeedId = seedId, RecipeName = seededData.RecipeName };
}
private SceneResult ExecuteRecipeMethod(string templateName, JsonElement? arguments, string methodName)
private SceneResult<object?> ExecuteSceneMethod(string templateName, JsonElement? arguments, string methodName)
{
try
{