Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,19 @@
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": "Unified",
"requireExactSource": true,
"type": "coreclr",
"request": "launch",
"program": "${workspaceFolder}/src/Unified/bin/Debug/net8.0/Unified.dll",
"cwd": "${workspaceFolder}/src/Unified",
"stopAtEntry": false,
"env": {
"ASPNETCORE_ENVIRONMENT": "Development",
},
"preLaunchTask": "buildUnified"
},
{
"name": ".NET Core Attach",
"type": "coreclr",
Expand Down
13 changes: 13 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,19 @@
"isDefault": true
}
},
{
"label": "buildUnified",
"hide": false,
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/src/Unified/Unified.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile",
},
{
"label": "test",
"type": "shell",
Expand Down
16 changes: 16 additions & 0 deletions bitwarden-server.sln
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server.IntegrationTest", "t
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Setup.Test", "test\Setup.Test\Setup.Test.csproj", "{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4182C379-9AA2-4B7C-9B32-AEA30819515B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unified", "src\Unified\Unified.csproj", "{EC5047DF-274C-4865-A615-6CA898A58369}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Unified.Test", "test\Unified.Test\Unified.Test.csproj", "{E7043D94-D8ED-451E-B6D3-26EA3D96F132}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -385,6 +391,14 @@ Global
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED}.Release|Any CPU.Build.0 = Release|Any CPU
{EC5047DF-274C-4865-A615-6CA898A58369}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EC5047DF-274C-4865-A615-6CA898A58369}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC5047DF-274C-4865-A615-6CA898A58369}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC5047DF-274C-4865-A615-6CA898A58369}.Release|Any CPU.Build.0 = Release|Any CPU
{E7043D94-D8ED-451E-B6D3-26EA3D96F132}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E7043D94-D8ED-451E-B6D3-26EA3D96F132}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E7043D94-D8ED-451E-B6D3-26EA3D96F132}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E7043D94-D8ED-451E-B6D3-26EA3D96F132}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -447,6 +461,8 @@ Global
{FFB09376-595B-6F93-36F0-70CAE90AFECB} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B}
{E75E1F10-BC6F-4EB1-BA75-D897C45AEA0D} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{5A3AB73D-F0E8-4DC6-B072-0D3B394621ED} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
{EC5047DF-274C-4865-A615-6CA898A58369} = {4182C379-9AA2-4B7C-9B32-AEA30819515B}
{E7043D94-D8ED-451E-B6D3-26EA3D96F132} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F}
Expand Down
46 changes: 46 additions & 0 deletions src/Unified/ApplicationConfigurator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Reflection;

namespace Bit.Unified;

// TODO: Add way to map endpoints only
public interface IApplicationConfigurator
{
Assembly AppAssembly { get; }
string RoutePrefix { get; }
void ConfigureServices(WebApplicationBuilder builder, IServiceCollection services);
void Configure(WebApplication app, IApplicationBuilder builder);
}

public class ApplicationConfigurator<T> : IApplicationConfigurator
where T : class
{
private readonly T _instance;
private readonly Action<T, WebApplicationBuilder, IServiceCollection> _configureServices;
private readonly Action<T, WebApplication, IApplicationBuilder> _configure;

public ApplicationConfigurator(
string routePrefix,
T instance,
Action<T, WebApplicationBuilder, IServiceCollection> configureServices,
Action<T, WebApplication, IApplicationBuilder> configure)
{
AppAssembly = typeof(T).Assembly;
RoutePrefix = routePrefix;
_instance = instance;
_configureServices = configureServices;
_configure = configure;
}

public Assembly AppAssembly { get; }
public string RoutePrefix { get; }

public void ConfigureServices(WebApplicationBuilder builder, IServiceCollection services)
{
_configureServices(_instance, builder, services);
}

public void Configure(WebApplication app, IApplicationBuilder builder)
{
_configure(_instance, app, builder);
}
}
93 changes: 93 additions & 0 deletions src/Unified/AssemblyRoutingConvention.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
๏ปฟusing System.Collections.Frozen;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace Bit.Unified;

public class AssemblyRoutingConvention : IControllerModelConvention
{
private readonly FrozenDictionary<Assembly, string> _services;
private readonly ILogger _logger;

public AssemblyRoutingConvention(IEnumerable<IApplicationConfigurator> services, ILogger logger)
{
_services = services.ToFrozenDictionary(s => s.AppAssembly, s => s.RoutePrefix);
_logger = logger;
}

public void Apply(ControllerModel controller)
{
if (!_services.TryGetValue(controller.ControllerType.Assembly, out var prefix))
{
// TODO: Should we warn?
_logger.LogWarning("Controller {ControllerType} belongs in an assembly that is not configured", controller.ControllerType.FullName);
return;
}

// In case it is fully convention based, insert the prefix route value
controller.RouteValues.Add("prefix", prefix);

if (controller.Selectors.Count == 0)
{
throw new NotImplementedException("What does this look like.");
}

foreach (var selector in controller.Selectors)
{
if (selector.AttributeRouteModel is not null)
{
var template = GetTrimmedOverridePattern(selector.AttributeRouteModel.Template);
selector.AttributeRouteModel.Template = AttributeRouteModel.CombineTemplates(prefix, template);
}
else
{
selector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(prefix));
}
}

foreach (var actionSelector in controller.Actions.SelectMany(a => a.Selectors))
{
if (actionSelector.AttributeRouteModel is null)
{
// If it's null let convention and our addition of the controller selector do its work
continue;
}

var template = actionSelector.AttributeRouteModel.Template;

if (template is null)
{
continue;
}
else if (template.StartsWith('/'))
{
// Add back the override pattern at the beginning
actionSelector.AttributeRouteModel.Template = $"/{prefix}/{template[1..]}";
}
else if (template.StartsWith("~/", StringComparison.Ordinal))
{
// TODO: Test this more
actionSelector.AttributeRouteModel.Template = $"~/{prefix}/{template[2..]}";
}
}
}

private static string? GetTrimmedOverridePattern(string? template)
{
if (template == null)
{
return null;
}
else if (template.StartsWith('/'))
{
return template[1..];
}
else if (template.StartsWith("~/", StringComparison.Ordinal))
{
return template[2..];
}

return template;
}
}
118 changes: 118 additions & 0 deletions src/Unified/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
๏ปฟusing Bit.Core.Settings;
using Bit.Unified;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// TODO: Configure global settings
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
// Unified is always ran as self hosted.
{ "GlobalSettings:SelfHosted", "true" },
// TODO: Remove
{ "GlobalSettings:Installation:Id", Guid.NewGuid().ToString() },
});

IEnumerable<IApplicationConfigurator> services = [
// Bootstap Identity
new ApplicationConfigurator<Bit.Identity.Startup>(
"identity",
new Bit.Identity.Startup(builder.Environment, builder.Configuration),
static (startup, builder, services) => startup.ConfigureServices(services),
static (startup, app, builder) =>
{
startup.Configure(
builder,
app.Environment,
app.Services.GetRequiredService<GlobalSettings>(),
app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<Bit.Identity.Startup>()
);
}
),
// Bootstrap API
new ApplicationConfigurator<Bit.Api.Startup>(
"api",
new Bit.Api.Startup(builder.Environment, builder.Configuration),
static (startup, builder, services) => startup.ConfigureServices(services),
static (startup, app, builder) =>
{
startup.Configure(
builder,
app.Environment,
app.Services.GetRequiredService<GlobalSettings>(),
app.Services.GetRequiredService<ILoggerFactory>().CreateLogger<Bit.Api.Startup>()
);
}
),
// Bootstrap Admin
new ApplicationConfigurator<Bit.Admin.Startup>(
"admin",
new Bit.Admin.Startup(builder.Environment, builder.Configuration),
static (startup, builder, services) => startup.ConfigureServices(services),
static (startup, app, builder) =>
{
startup.Configure(
builder,
app.Environment,
app.Services.GetRequiredService<GlobalSettings>()
);
}
)
];

builder.Services.AddOptions<MvcOptions>()
.Configure<ILoggerFactory>((options, loggerFactory) =>
{
options.Conventions.Add(new AssemblyRoutingConvention(
services,
loggerFactory.CreateLogger("Bitwarden.Unified")
));
});

// TODO: Place happy path overrides here

foreach (var service in services)
{
service.ConfigureServices(builder, builder.Services);
}

// TODO: Add overriding services

var app = builder.Build();

var globalSettings = app.Services.GetRequiredService<GlobalSettings>();

// TODO: Middleware
// foreach (var service in services)
// {
// app.MapWhen(
// c => c.Request.Path.StartsWithSegments("/" + service.RoutePrefix),
// builder =>
// {
// // Map their specific middleware
// service.Configure(app, builder);
// }
// );
// }

app.MapGet("/endpoints", (EndpointDataSource endpoints, ILoggerFactory loggerFactory) =>

Check failure

Code scanning / Checkmarx One

Reflected XSS High

Reflected XSS
{
var logger = loggerFactory.CreateLogger("Bitwarden.Unified");
foreach (var e in endpoints.Endpoints.OfType<RouteEndpoint>().Select(e => e.RoutePattern.RawText))
{
logger.LogWarning("Endpoint: {Route}", e);
}
return TypedResults.NoContent();
});

// foreach (var service in services)
// {
// var group = app.MapGroup("/" + service.RoutePrefix);
// service.MapEndpoints(group);
// }

app.MapControllers();

app.Run();

public partial class Program;
41 changes: 41 additions & 0 deletions src/Unified/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
๏ปฟ{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:34079",
"sslPort": 44369
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5022",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7035;http://localhost:5022",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
20 changes: 20 additions & 0 deletions src/Unified/Unified.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>bitwarden-Api</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Api\Api.csproj" />
<ProjectReference Include="..\Identity\Identity.csproj" />
<ProjectReference Include="..\Admin\Admin.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisisbleTo Include="Unified.Test" />
</ItemGroup>

</Project>
Loading
Loading