1
0
mirror of https://github.com/bitwarden/server synced 2026-02-26 01:13:35 +00:00
Files
server/src/Api/Startup.cs
Oscar Hinton f144828a87 [PM-22263] [PM-29849] Initial PoC of seeder API (#6424)
We want to reduce the amount of business critical test data in the company. One way of doing that is to generate test data on demand prior to client side testing.

Clients will request a scene to be set up with a JSON body set of options, specific to a given scene. Successful seed requests will be responded to with a mangleMap which maps magic strings present in the request to the mangled, non-colliding versions inserted into the database. This way, the server is solely responsible for understanding uniqueness requirements in the database. scenes also are able to return custom data, depending on the scene. For example, user creation would benefit from a return value of the userId for further test setup on the client side.

Clients will indicate they are running tests by including a unique header, x-play-id which specifies a unique testing context. The server uses this PlayId as the seed for any mangling that occurs. This allows the client to decide it will reuse a given PlayId if the test context builds on top of previously executed tests. When a given context is no longer needed, the API user will delete all test data associated with the PlayId by calling a delete endpoint.

---------

Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
2026-01-13 11:10:01 -06:00

370 lines
14 KiB
C#

using Bit.Api.Utilities;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Settings;
using AspNetCoreRateLimit;
using Stripe;
using Bit.Core.Utilities;
using Duende.IdentityModel;
using System.Globalization;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
using Bit.Core.Auth.Entities;
using Bit.SharedWeb.Health;
using Microsoft.IdentityModel.Logging;
using Microsoft.OpenApi.Models;
using Bit.SharedWeb.Utilities;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Bit.Core.Auth.UserFeatures;
using Bit.Core.Entities;
using Bit.Core.Billing.Extensions;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Dirt.Reports.ReportFeatures;
using Bit.Core.Tools.SendFeatures;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.Identity;
using Bit.Core.Enums;
#if !OSS
using Bit.Commercial.Core.SecretsManager;
using Bit.Commercial.Core.Utilities;
using Bit.Commercial.Infrastructure.EntityFramework.SecretsManager;
#endif
namespace Bit.Api;
public class Startup
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; private set; }
public IWebHostEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
if (!globalSettings.SelfHosted)
{
services.Configure<IpRateLimitOptions>(Configuration.GetSection("IpRateLimitOptions"));
services.Configure<IpRateLimitPolicies>(Configuration.GetSection("IpRateLimitPolicies"));
}
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Event Grid
if (!string.IsNullOrWhiteSpace(globalSettings.EventGridKey))
{
ApiHelpers.EventGridKey = globalSettings.EventGridKey;
}
// Stripe Billing
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Repositories
services.AddDatabaseRepositories(globalSettings);
services.AddTestPlayIdTracking(globalSettings);
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
// Caching
services.AddMemoryCache();
services.AddDistributedCache(globalSettings);
if (!globalSettings.SelfHosted)
{
services.AddIpRateLimiting(globalSettings);
}
// Identity
services.AddCustomIdentityServices(globalSettings);
services.AddIdentityAuthenticationServices(globalSettings, Environment, config =>
{
config.AddPolicy(Policies.Application, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
});
config.AddPolicy(Policies.Web, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.AuthenticationMethod, "Application", "external");
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.Api);
policy.RequireClaim(JwtClaimTypes.ClientId, BitwardenClient.Web);
});
config.AddPolicy(Policies.Push, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiPush);
});
config.AddPolicy(Policies.Licensing, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiLicensing);
});
config.AddPolicy(Policies.Organization, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiOrganization);
});
config.AddPolicy(Policies.Installation, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiInstallation);
});
config.AddPolicy(Policies.Secrets, policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireAssertion(ctx => ctx.User.HasClaim(c =>
c.Type == JwtClaimTypes.Scope &&
(c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets))
));
});
config.AddPolicy(Policies.Send, configurePolicy: policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiSendAccess);
policy.RequireClaim(Claims.SendAccessClaims.SendId);
});
});
services.AddScoped<AuthenticatorTokenProvider>();
// Key Rotation
services.AddUserKeyCommands(globalSettings);
services
.AddScoped<IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>>,
CipherRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>>,
FolderRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>>,
SendRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>,
EmergencyAccessRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
, OrganizationUserRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>,
WebAuthnLoginKeyRotationValidator>();
services
.AddScoped<IRotationValidator<IEnumerable<OtherDeviceKeysUpdateRequestModel>, IEnumerable<Device>>,
DeviceRotationValidator>();
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddOrganizationSubscriptionServices();
services.AddCoreLocalizationServices();
services.AddBillingOperations();
services.AddReportingServices();
services.AddImportServices();
services.AddSendServices();
// Authorization Handlers
services.AddAuthorizationHandlers();
//health check
if (!globalSettings.SelfHosted)
{
services.AddHealthChecks(globalSettings);
}
#if OSS
services.AddOosServices();
#else
services.AddCommercialCoreServices();
services.AddCommercialSecretsManagerServices();
services.AddSecretsManagerEfRepositories();
Jobs.JobsHostedService.AddCommercialSecretsManagerJobServices(services);
#endif
// MVC
services.AddMvc(config =>
{
config.Conventions.Add(new ApiExplorerGroupConvention());
config.Conventions.Add(new PublicApiControllersModelConvention());
});
services.AddSwaggerGen(globalSettings, Environment);
Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted);
services.AddHostedService<Jobs.JobsHostedService>();
if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName))
{
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
}
// Add Event Integrations services
services.AddEventIntegrationsCommandsQueries(globalSettings);
services.AddSlackService(globalSettings);
services.AddTeamsService(globalSettings);
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
GlobalSettings globalSettings,
ILogger<Startup> logger)
{
IdentityModelEventSource.ShowPII = true;
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
// Default Middleware
app.UseDefaultMiddleware(env, globalSettings);
if (!globalSettings.SelfHosted)
{
// Rate limiting
app.UseMiddleware<CustomIpRateLimitMiddleware>();
}
else
{
app.UseForwardedHeaders(globalSettings);
}
// Add localization
app.UseCoreLocalization();
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add routing
app.UseRouting();
// Add Cors
app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
// Add authentication and authorization to the request pipeline.
app.UseAuthentication();
app.UseAuthorization();
// Add current context
app.UseMiddleware<CurrentContextMiddleware>();
// Add endpoints to the request pipeline.
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
if (!globalSettings.SelfHosted)
{
endpoints.MapHealthChecks("/healthz");
endpoints.MapHealthChecks("/healthz/extended", new HealthCheckOptions
{
ResponseWriter = HealthCheckServiceExtensions.WriteResponse
});
}
});
// Add Swagger
// Note that the swagger.json generation is configured in the call to AddSwaggerGen above.
if (Environment.IsDevelopment() || globalSettings.SelfHosted)
{
// adds the middleware to serve the swagger.json while the server is running
app.UseSwagger(config =>
{
config.RouteTemplate = "specs/{documentName}/swagger.json";
// Remove all Bitwarden cloud servers and only register the local server
config.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
{
swaggerDoc.Servers.Clear();
swaggerDoc.Servers.Add(new OpenApiServer
{
Url = globalSettings.BaseServiceUri.Api,
});
swaggerDoc.Components.SecuritySchemes.Clear();
swaggerDoc.Components.SecuritySchemes.Add("oauth2-client-credentials", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
ClientCredentials = new OpenApiOAuthFlow
{
TokenUrl = new Uri($"{globalSettings.BaseServiceUri.Identity}/connect/token"),
Scopes = new Dictionary<string, string>
{
{ ApiScopes.ApiOrganization, "Organization APIs" }
}
}
}
});
swaggerDoc.SecurityRequirements.Clear();
swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2-client-credentials"
}
},
[ApiScopes.ApiOrganization]
}
});
});
});
// adds the middleware to display the web UI
app.UseSwaggerUI(config =>
{
config.DocumentTitle = "Bitwarden API Documentation";
config.RoutePrefix = "docs";
config.SwaggerEndpoint($"{globalSettings.BaseServiceUri.Api}/specs/public/swagger.json",
"Bitwarden Public API");
config.OAuthClientId("accountType.id");
config.OAuthClientSecret("secretKey");
// Persist authorization on page refresh - for development use only
if (Environment.IsDevelopment())
{
config.EnablePersistAuthorization();
}
});
}
// Log startup
logger.LogInformation(Constants.BypassFiltersEventId, "{Project} started.", globalSettings.ProjectName);
}
}